From b32e6f3a3acd543f17c39fa0696fd8fac05447be Mon Sep 17 00:00:00 2001 From: Molly Xu Date: Wed, 3 Dec 2025 12:40:54 -0800 Subject: [PATCH 1/5] expose cpu_fallback --- src/torchcodec/decoders/__init__.py | 2 +- src/torchcodec/decoders/_video_decoder.py | 85 ++++++++++++++++++++++ test/test_decoders.py | 86 +++++++++++++++++++++++ 3 files changed, 172 insertions(+), 1 deletion(-) diff --git a/src/torchcodec/decoders/__init__.py b/src/torchcodec/decoders/__init__.py index 980ba98a9..a07760a60 100644 --- a/src/torchcodec/decoders/__init__.py +++ b/src/torchcodec/decoders/__init__.py @@ -7,6 +7,6 @@ from .._core import AudioStreamMetadata, VideoStreamMetadata from ._audio_decoder import AudioDecoder # noqa from ._decoder_utils import set_cuda_backend # noqa -from ._video_decoder import VideoDecoder # noqa +from ._video_decoder import FallbackInfo, VideoDecoder # noqa SimpleVideoDecoder = VideoDecoder diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 1b4d4706d..c0eb67e4b 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -7,6 +7,7 @@ import io import json import numbers +from dataclasses import dataclass from pathlib import Path from typing import List, Literal, Optional, Sequence, Tuple, Union @@ -22,6 +23,48 @@ from torchcodec.transforms import DecoderTransform, Resize +@dataclass +class FallbackInfo: + """Information about decoder fallback status. + + This class tracks whether hardware-accelerated decoding failed and the decoder + fell back to software decoding. + + Usage: + - Use ``str(fallback_info)`` or ``print(fallback_info)`` to see the fallback status + - Use ``bool(fallback_info)`` to check if any fallback occurred + + Attributes: + status_known (bool): Whether the fallback status has been determined. + """ + + def __init__(self): + self.status_known = False + self.__nvcuvid_unavailable = False + self.__video_not_supported = False + + def __bool__(self): + """Returns True if fallback occurred (and status is known).""" + return self.status_known and ( + self.__nvcuvid_unavailable or self.__video_not_supported + ) + + def __str__(self): + """Returns a human-readable string representation of the fallback status.""" + if not self.status_known: + return "Fallback status: Unknown" + + reasons = [] + if self.__nvcuvid_unavailable: + reasons.append("NVcuvid unavailable") + if self.__video_not_supported: + reasons.append("Video not supported") + + if reasons: + return "Fallback status: Falling back due to: " + ", ".join(reasons) + return "Fallback status: No fallback required" + + class VideoDecoder: """A single-stream video decoder. @@ -180,13 +223,48 @@ def __init__( custom_frame_mappings=custom_frame_mappings_data, ) + # Initialize fallback info + self._fallback_info = FallbackInfo() + def __len__(self) -> int: return self._num_frames + @property + def cpu_fallback(self) -> FallbackInfo: + """Get information about decoder fallback status. + + Returns: + FallbackInfo: Information about whether hardware-accelerated decoding + failed and the decoder fell back to software decoding. + + Note: + The fallback status is only determined after the first frame access. + Before that, the status will be "Unknown". + """ + return self._fallback_info + + def _update_cpu_fallback(self): + """Update the fallback status if it hasn't been determined yet. + + This method should be called after any frame decoding operation to determine + if fallback to software decoding occurred. + """ + if not self._fallback_info.status_known: + backend_details = core._get_backend_details(self._decoder) + + self._fallback_info.status_known = True + + if "CPU fallback" in backend_details: + if "NVCUVID not available" in backend_details: + self._fallback_info._FallbackInfo__nvcuvid_unavailable = True + else: + self._fallback_info._FallbackInfo__video_not_supported = True + def _getitem_int(self, key: int) -> Tensor: assert isinstance(key, int) frame_data, *_ = core.get_frame_at_index(self._decoder, frame_index=key) + self._update_cpu_fallback() return frame_data def _getitem_slice(self, key: slice) -> Tensor: @@ -199,6 +277,7 @@ def _getitem_slice(self, key: slice) -> Tensor: stop=stop, step=step, ) + self._update_cpu_fallback() return frame_data def __getitem__(self, key: Union[numbers.Integral, slice]) -> Tensor: @@ -252,6 +331,7 @@ def get_frame_at(self, index: int) -> Frame: data, pts_seconds, duration_seconds = core.get_frame_at_index( self._decoder, frame_index=index ) + self._update_cpu_fallback() return Frame( data=data, pts_seconds=pts_seconds.item(), @@ -271,6 +351,7 @@ def get_frames_at(self, indices: Union[torch.Tensor, list[int]]) -> FrameBatch: data, pts_seconds, duration_seconds = core.get_frames_at_indices( self._decoder, frame_indices=indices ) + self._update_cpu_fallback() return FrameBatch( data=data, @@ -300,6 +381,7 @@ def get_frames_in_range(self, start: int, stop: int, step: int = 1) -> FrameBatc stop=stop, step=step, ) + self._update_cpu_fallback() return FrameBatch(*frames) def get_frame_played_at(self, seconds: float) -> Frame: @@ -329,6 +411,7 @@ def get_frame_played_at(self, seconds: float) -> Frame: data, pts_seconds, duration_seconds = core.get_frame_at_pts( self._decoder, seconds ) + self._update_cpu_fallback() return Frame( data=data, pts_seconds=pts_seconds.item(), @@ -350,6 +433,7 @@ def get_frames_played_at( data, pts_seconds, duration_seconds = core.get_frames_by_pts( self._decoder, timestamps=seconds ) + self._update_cpu_fallback() return FrameBatch( data=data, pts_seconds=pts_seconds, @@ -394,6 +478,7 @@ def get_frames_played_in_range( start_seconds=start_seconds, stop_seconds=stop_seconds, ) + self._update_cpu_fallback() return FrameBatch(*frames) diff --git a/test/test_decoders.py b/test/test_decoders.py index efa2d11c8..d85387f8b 100644 --- a/test/test_decoders.py +++ b/test/test_decoders.py @@ -1737,6 +1737,92 @@ def test_set_cuda_backend(self): with set_cuda_backend(backend): VideoDecoder(H265_VIDEO.path, device=f"cuda:{bad_device_number}") + def test_cpu_fallback_before_after_decoding(self): + decoder = VideoDecoder(NASA_VIDEO.path) + + # Before accessing any frames, status should be unknown + assert not decoder.cpu_fallback.status_known + assert str(decoder.cpu_fallback) == "Fallback status: Unknown" + assert not bool(decoder.cpu_fallback) + + # After accessing frames, status should be known + _ = decoder[0] + assert decoder.cpu_fallback.status_known + assert str(decoder.cpu_fallback) != "Fallback status: Unknown" + + def test_cpu_fallback_no_fallback_on_cpu_device(self): + """Test that CPU device doesn't trigger fallback (it's not a fallback scenario).""" + decoder = VideoDecoder(NASA_VIDEO.path, device="cpu") + + _ = decoder[0] + + assert decoder.cpu_fallback.status_known + assert not bool(decoder.cpu_fallback) + assert "No fallback required" in str(decoder.cpu_fallback) + + @needs_cuda + def test_cpu_fallback_h265_video_ffmpeg_cuda(self): + """Test that H265 video triggers CPU fallback on FFmpeg CUDA interface.""" + # H265_VIDEO is known to trigger CPU fallback on FFmpeg CUDA + # because its dimensions are too small + decoder = VideoDecoder(H265_VIDEO.path, device="cuda") + + _ = decoder.get_frame_at(0) + + assert decoder.cpu_fallback.status_known + assert bool(decoder.cpu_fallback) + assert "Fallback status: Falling back due to:" in str(decoder.cpu_fallback) + + @needs_cuda + def test_cpu_fallback_h265_video_beta_cuda(self): + """Test that H265 video triggers CPU fallback on Beta CUDA interface.""" + with set_cuda_backend("beta"): + decoder = VideoDecoder(H265_VIDEO.path, device="cuda") + + _ = decoder.get_frame_at(0) + + assert decoder.cpu_fallback.status_known + assert bool(decoder.cpu_fallback) + assert "Fallback status: Falling back due to:" in str(decoder.cpu_fallback) + + @needs_cuda + def test_cpu_fallback_no_fallback_on_supported_video(self): + """Test that supported videos don't trigger fallback on CUDA.""" + decoder = VideoDecoder(NASA_VIDEO.path, device="cuda") + + # Access a frame to determine status + _ = decoder[0] + + assert not bool(decoder.cpu_fallback) + + def test_cpu_fallback_status_cached(self): + """Test that cpu_fallback status is determined once and then cached.""" + decoder = VideoDecoder(NASA_VIDEO.path) + + _ = decoder[0] + first_status = str(decoder.cpu_fallback) + assert decoder.cpu_fallback.status_known + + _ = decoder[1] + second_status = str(decoder.cpu_fallback) + assert decoder.cpu_fallback.status_known + + assert first_status == second_status + + def test_cpu_fallback_multiple_access_methods(self): + """Test that cpu_fallback works with different frame access methods.""" + decoder = VideoDecoder(NASA_VIDEO.path) + + _ = decoder.get_frame_at(0) + assert decoder.cpu_fallback.status_known + status_after_get_frame = str(decoder.cpu_fallback) + + _ = decoder.get_frames_in_range(1, 3) + assert str(decoder.cpu_fallback) == status_after_get_frame + + _ = decoder.get_frame_played_at(0.5) + assert str(decoder.cpu_fallback) == status_after_get_frame + class TestAudioDecoder: @pytest.mark.parametrize("asset", (NASA_AUDIO, NASA_AUDIO_MP3, SINE_MONO_S32)) From cf5b718f988b6a34bc8c43c3602b08583022a584 Mon Sep 17 00:00:00 2001 From: Molly Xu Date: Wed, 3 Dec 2025 12:53:51 -0800 Subject: [PATCH 2/5] modify comments --- src/torchcodec/decoders/_video_decoder.py | 22 ++++++---------------- test/test_decoders.py | 2 +- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index c0eb67e4b..0c5586e61 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -27,11 +27,10 @@ class FallbackInfo: """Information about decoder fallback status. - This class tracks whether hardware-accelerated decoding failed and the decoder - fell back to software decoding. + This class tracks whether the decoder fell back to CPU decoding. Usage: - - Use ``str(fallback_info)`` or ``print(fallback_info)`` to see the fallback status + - Use ``str(fallback_info)`` or ``print(fallback_info)`` to see the cpu fallback status - Use ``bool(fallback_info)`` to check if any fallback occurred Attributes: @@ -44,13 +43,13 @@ def __init__(self): self.__video_not_supported = False def __bool__(self): - """Returns True if fallback occurred (and status is known).""" + """Returns True if fallback occurred.""" return self.status_known and ( self.__nvcuvid_unavailable or self.__video_not_supported ) def __str__(self): - """Returns a human-readable string representation of the fallback status.""" + """Returns a human-readable string representation of the cpu fallback status.""" if not self.status_known: return "Fallback status: Unknown" @@ -223,7 +222,6 @@ def __init__( custom_frame_mappings=custom_frame_mappings_data, ) - # Initialize fallback info self._fallback_info = FallbackInfo() def __len__(self) -> int: @@ -231,16 +229,8 @@ def __len__(self) -> int: @property def cpu_fallback(self) -> FallbackInfo: - """Get information about decoder fallback status. - - Returns: - FallbackInfo: Information about whether hardware-accelerated decoding - failed and the decoder fell back to software decoding. - - Note: - The fallback status is only determined after the first frame access. - Before that, the status will be "Unknown". - """ + # We can only determine whether fallback to CPU is happening after + # the first frame access. Before that, the status will be "Unknown". return self._fallback_info def _update_cpu_fallback(self): diff --git a/test/test_decoders.py b/test/test_decoders.py index d85387f8b..a54496d97 100644 --- a/test/test_decoders.py +++ b/test/test_decoders.py @@ -1790,10 +1790,10 @@ def test_cpu_fallback_no_fallback_on_supported_video(self): """Test that supported videos don't trigger fallback on CUDA.""" decoder = VideoDecoder(NASA_VIDEO.path, device="cuda") - # Access a frame to determine status _ = decoder[0] assert not bool(decoder.cpu_fallback) + assert "No fallback required" in str(decoder.cpu_fallback) def test_cpu_fallback_status_cached(self): """Test that cpu_fallback status is determined once and then cached.""" From 6e69c8ca771d686d1e55460628538559d515543d Mon Sep 17 00:00:00 2001 From: Molly Xu Date: Wed, 3 Dec 2025 15:11:22 -0800 Subject: [PATCH 3/5] modify comments --- src/torchcodec/decoders/_video_decoder.py | 29 +++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 0c5586e61..f8046a0da 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -223,23 +223,26 @@ def __init__( ) self._fallback_info = FallbackInfo() + self._has_decoded_frame = False def __len__(self) -> int: return self._num_frames @property def cpu_fallback(self) -> FallbackInfo: - # We can only determine whether fallback to CPU is happening after - # the first frame access. Before that, the status will be "Unknown". + # We can only determine whether fallback to CPU is happening when this + # property is accessed and requires that at least one frame has been decoded. + self._update_cpu_fallback() return self._fallback_info def _update_cpu_fallback(self): """Update the fallback status if it hasn't been determined yet. - This method should be called after any frame decoding operation to determine - if fallback to software decoding occurred. + This method queries the C++ backend to determine if fallback to CPU + decoding occurred. The query is only performed after at least one frame + has been decoded. """ - if not self._fallback_info.status_known: + if not self._fallback_info.status_known and self._has_decoded_frame: backend_details = core._get_backend_details(self._decoder) self._fallback_info.status_known = True @@ -254,7 +257,7 @@ def _getitem_int(self, key: int) -> Tensor: assert isinstance(key, int) frame_data, *_ = core.get_frame_at_index(self._decoder, frame_index=key) - self._update_cpu_fallback() + self._has_decoded_frame = True return frame_data def _getitem_slice(self, key: slice) -> Tensor: @@ -267,7 +270,7 @@ def _getitem_slice(self, key: slice) -> Tensor: stop=stop, step=step, ) - self._update_cpu_fallback() + self._has_decoded_frame = True return frame_data def __getitem__(self, key: Union[numbers.Integral, slice]) -> Tensor: @@ -321,7 +324,7 @@ def get_frame_at(self, index: int) -> Frame: data, pts_seconds, duration_seconds = core.get_frame_at_index( self._decoder, frame_index=index ) - self._update_cpu_fallback() + self._has_decoded_frame = True return Frame( data=data, pts_seconds=pts_seconds.item(), @@ -341,7 +344,7 @@ def get_frames_at(self, indices: Union[torch.Tensor, list[int]]) -> FrameBatch: data, pts_seconds, duration_seconds = core.get_frames_at_indices( self._decoder, frame_indices=indices ) - self._update_cpu_fallback() + self._has_decoded_frame = True return FrameBatch( data=data, @@ -371,7 +374,7 @@ def get_frames_in_range(self, start: int, stop: int, step: int = 1) -> FrameBatc stop=stop, step=step, ) - self._update_cpu_fallback() + self._has_decoded_frame = True return FrameBatch(*frames) def get_frame_played_at(self, seconds: float) -> Frame: @@ -401,7 +404,7 @@ def get_frame_played_at(self, seconds: float) -> Frame: data, pts_seconds, duration_seconds = core.get_frame_at_pts( self._decoder, seconds ) - self._update_cpu_fallback() + self._has_decoded_frame = True return Frame( data=data, pts_seconds=pts_seconds.item(), @@ -423,7 +426,7 @@ def get_frames_played_at( data, pts_seconds, duration_seconds = core.get_frames_by_pts( self._decoder, timestamps=seconds ) - self._update_cpu_fallback() + self._has_decoded_frame = True return FrameBatch( data=data, pts_seconds=pts_seconds, @@ -468,7 +471,7 @@ def get_frames_played_in_range( start_seconds=start_seconds, stop_seconds=stop_seconds, ) - self._update_cpu_fallback() + self._has_decoded_frame = True return FrameBatch(*frames) From 5ac83215c72b301705df23532918d6a63c7e88f3 Mon Sep 17 00:00:00 2001 From: Molly Xu Date: Thu, 4 Dec 2025 11:03:32 -0800 Subject: [PATCH 4/5] address feedback: --- src/torchcodec/_core/CudaDeviceInterface.cpp | 6 ++ src/torchcodec/_core/CudaDeviceInterface.h | 1 + src/torchcodec/decoders/__init__.py | 2 +- src/torchcodec/decoders/_video_decoder.py | 62 ++++++++------------ test/test_decoders.py | 20 +++---- 5 files changed, 40 insertions(+), 51 deletions(-) diff --git a/src/torchcodec/_core/CudaDeviceInterface.cpp b/src/torchcodec/_core/CudaDeviceInterface.cpp index 0e20c5e8d..67c274136 100644 --- a/src/torchcodec/_core/CudaDeviceInterface.cpp +++ b/src/torchcodec/_core/CudaDeviceInterface.cpp @@ -241,6 +241,8 @@ void CudaDeviceInterface::convertAVFrameToFrameOutput( std::optional preAllocatedOutputTensor) { validatePreAllocatedTensorShape(preAllocatedOutputTensor, avFrame); + hasDecodedFrame_ = true; + // All of our CUDA decoding assumes NV12 format. We handle non-NV12 formats by // converting them to NV12. avFrame = maybeConvertAVFrameToNV12OrRGB24(avFrame); @@ -358,6 +360,10 @@ 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. + if (!hasDecodedFrame_) { + return std::string( + "FFmpeg CUDA Device Interface. Fallback status unknown (no frames decoded)."); + } return std::string("FFmpeg CUDA Device Interface. Using ") + (usingCPUFallback_ ? "CPU fallback." : "NVDEC."); } diff --git a/src/torchcodec/_core/CudaDeviceInterface.h b/src/torchcodec/_core/CudaDeviceInterface.h index c892bd49b..90d359185 100644 --- a/src/torchcodec/_core/CudaDeviceInterface.h +++ b/src/torchcodec/_core/CudaDeviceInterface.h @@ -63,6 +63,7 @@ class CudaDeviceInterface : public DeviceInterface { std::unique_ptr nv12Conversion_; bool usingCPUFallback_ = false; + bool hasDecodedFrame_ = false; }; } // namespace facebook::torchcodec diff --git a/src/torchcodec/decoders/__init__.py b/src/torchcodec/decoders/__init__.py index a07760a60..ef08cce83 100644 --- a/src/torchcodec/decoders/__init__.py +++ b/src/torchcodec/decoders/__init__.py @@ -7,6 +7,6 @@ from .._core import AudioStreamMetadata, VideoStreamMetadata from ._audio_decoder import AudioDecoder # noqa from ._decoder_utils import set_cuda_backend # noqa -from ._video_decoder import FallbackInfo, VideoDecoder # noqa +from ._video_decoder import CpuFallbackStatus, VideoDecoder # noqa SimpleVideoDecoder = VideoDecoder diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index f8046a0da..54dec7bf4 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -24,14 +24,14 @@ @dataclass -class FallbackInfo: - """Information about decoder fallback status. +class CpuFallbackStatus: + """Information about CPU fallback status. This class tracks whether the decoder fell back to CPU decoding. Usage: - - Use ``str(fallback_info)`` or ``print(fallback_info)`` to see the cpu fallback status - - Use ``bool(fallback_info)`` to check if any fallback occurred + - Use ``str(cpu_fallback_status)`` or ``print(cpu_fallback_status)`` to see the cpu fallback status + - Use ``bool(cpu_fallback_status)`` to check if any fallback occurred Attributes: status_known (bool): Whether the fallback status has been determined. @@ -39,13 +39,13 @@ class FallbackInfo: def __init__(self): self.status_known = False - self.__nvcuvid_unavailable = False - self.__video_not_supported = False + self._nvcuvid_unavailable = False + self._video_not_supported = False def __bool__(self): """Returns True if fallback occurred.""" return self.status_known and ( - self.__nvcuvid_unavailable or self.__video_not_supported + self._nvcuvid_unavailable or self._video_not_supported ) def __str__(self): @@ -54,9 +54,9 @@ def __str__(self): return "Fallback status: Unknown" reasons = [] - if self.__nvcuvid_unavailable: + if self._nvcuvid_unavailable: reasons.append("NVcuvid unavailable") - if self.__video_not_supported: + if self._video_not_supported: reasons.append("Video not supported") if reasons: @@ -142,6 +142,10 @@ class VideoDecoder: stream_index (int): The stream index that this decoder is retrieving frames from. If a stream index was provided at initialization, this is the same value. If it was left unspecified, this is the :term:`best stream`. + cpu_fallback (CpuFallbackStatus): Information about whether the decoder fell back to CPU + decoding. Use ``bool(cpu_fallback)`` to check if fallback occurred, or + ``str(cpu_fallback)`` to get a human-readable status message. The status is only + determined after at least one frame has been decoded. """ def __init__( @@ -222,42 +226,33 @@ def __init__( custom_frame_mappings=custom_frame_mappings_data, ) - self._fallback_info = FallbackInfo() - self._has_decoded_frame = False + self._cpu_fallback = CpuFallbackStatus() def __len__(self) -> int: return self._num_frames @property - def cpu_fallback(self) -> FallbackInfo: + def cpu_fallback(self) -> CpuFallbackStatus: # We can only determine whether fallback to CPU is happening when this # property is accessed and requires that at least one frame has been decoded. - self._update_cpu_fallback() - return self._fallback_info - - def _update_cpu_fallback(self): - """Update the fallback status if it hasn't been determined yet. - - This method queries the C++ backend to determine if fallback to CPU - decoding occurred. The query is only performed after at least one frame - has been decoded. - """ - if not self._fallback_info.status_known and self._has_decoded_frame: + if not self._cpu_fallback.status_known: backend_details = core._get_backend_details(self._decoder) - self._fallback_info.status_known = True + if "status unknown" not in backend_details: + self._cpu_fallback.status_known = True + + if "CPU fallback" in backend_details: + if "NVCUVID not available" in backend_details: + self._cpu_fallback._nvcuvid_unavailable = True + else: + self._cpu_fallback._video_not_supported = True - if "CPU fallback" in backend_details: - if "NVCUVID not available" in backend_details: - self._fallback_info._FallbackInfo__nvcuvid_unavailable = True - else: - self._fallback_info._FallbackInfo__video_not_supported = True + return self._cpu_fallback def _getitem_int(self, key: int) -> Tensor: assert isinstance(key, int) frame_data, *_ = core.get_frame_at_index(self._decoder, frame_index=key) - self._has_decoded_frame = True return frame_data def _getitem_slice(self, key: slice) -> Tensor: @@ -270,7 +265,6 @@ def _getitem_slice(self, key: slice) -> Tensor: stop=stop, step=step, ) - self._has_decoded_frame = True return frame_data def __getitem__(self, key: Union[numbers.Integral, slice]) -> Tensor: @@ -324,7 +318,6 @@ def get_frame_at(self, index: int) -> Frame: data, pts_seconds, duration_seconds = core.get_frame_at_index( self._decoder, frame_index=index ) - self._has_decoded_frame = True return Frame( data=data, pts_seconds=pts_seconds.item(), @@ -344,7 +337,6 @@ def get_frames_at(self, indices: Union[torch.Tensor, list[int]]) -> FrameBatch: data, pts_seconds, duration_seconds = core.get_frames_at_indices( self._decoder, frame_indices=indices ) - self._has_decoded_frame = True return FrameBatch( data=data, @@ -374,7 +366,6 @@ def get_frames_in_range(self, start: int, stop: int, step: int = 1) -> FrameBatc stop=stop, step=step, ) - self._has_decoded_frame = True return FrameBatch(*frames) def get_frame_played_at(self, seconds: float) -> Frame: @@ -404,7 +395,6 @@ def get_frame_played_at(self, seconds: float) -> Frame: data, pts_seconds, duration_seconds = core.get_frame_at_pts( self._decoder, seconds ) - self._has_decoded_frame = True return Frame( data=data, pts_seconds=pts_seconds.item(), @@ -426,7 +416,6 @@ def get_frames_played_at( data, pts_seconds, duration_seconds = core.get_frames_by_pts( self._decoder, timestamps=seconds ) - self._has_decoded_frame = True return FrameBatch( data=data, pts_seconds=pts_seconds, @@ -471,7 +460,6 @@ def get_frames_played_in_range( start_seconds=start_seconds, stop_seconds=stop_seconds, ) - self._has_decoded_frame = True return FrameBatch(*frames) diff --git a/test/test_decoders.py b/test/test_decoders.py index a54496d97..95034b259 100644 --- a/test/test_decoders.py +++ b/test/test_decoders.py @@ -1737,19 +1737,6 @@ def test_set_cuda_backend(self): with set_cuda_backend(backend): VideoDecoder(H265_VIDEO.path, device=f"cuda:{bad_device_number}") - def test_cpu_fallback_before_after_decoding(self): - decoder = VideoDecoder(NASA_VIDEO.path) - - # Before accessing any frames, status should be unknown - assert not decoder.cpu_fallback.status_known - assert str(decoder.cpu_fallback) == "Fallback status: Unknown" - assert not bool(decoder.cpu_fallback) - - # After accessing frames, status should be known - _ = decoder[0] - assert decoder.cpu_fallback.status_known - assert str(decoder.cpu_fallback) != "Fallback status: Unknown" - def test_cpu_fallback_no_fallback_on_cpu_device(self): """Test that CPU device doesn't trigger fallback (it's not a fallback scenario).""" decoder = VideoDecoder(NASA_VIDEO.path, device="cpu") @@ -1767,6 +1754,8 @@ def test_cpu_fallback_h265_video_ffmpeg_cuda(self): # because its dimensions are too small decoder = VideoDecoder(H265_VIDEO.path, device="cuda") + assert not decoder.cpu_fallback.status_known + _ = decoder.get_frame_at(0) assert decoder.cpu_fallback.status_known @@ -1779,9 +1768,14 @@ def test_cpu_fallback_h265_video_beta_cuda(self): with set_cuda_backend("beta"): decoder = VideoDecoder(H265_VIDEO.path, device="cuda") + # Before accessing any frames, status should be unknown + assert decoder.cpu_fallback.status_known + _ = decoder.get_frame_at(0) + # After accessing frames, status should be known assert decoder.cpu_fallback.status_known + assert bool(decoder.cpu_fallback) assert "Fallback status: Falling back due to:" in str(decoder.cpu_fallback) From e97490e27d6cbd9db2ffaa38a0ec8bbaa902c23c Mon Sep 17 00:00:00 2001 From: Molly Xu Date: Thu, 4 Dec 2025 14:44:45 -0800 Subject: [PATCH 5/5] switch _.code._get_backend_details() to new api --- src/torchcodec/decoders/_video_decoder.py | 13 +++++- test/test_decoders.py | 49 +++++++++-------------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/torchcodec/decoders/_video_decoder.py b/src/torchcodec/decoders/_video_decoder.py index 54dec7bf4..b25904663 100644 --- a/src/torchcodec/decoders/_video_decoder.py +++ b/src/torchcodec/decoders/_video_decoder.py @@ -41,6 +41,7 @@ def __init__(self): self.status_known = False self._nvcuvid_unavailable = False self._video_not_supported = False + self._backend = "" def __bool__(self): """Returns True if fallback occurred.""" @@ -60,8 +61,11 @@ def __str__(self): reasons.append("Video not supported") if reasons: - return "Fallback status: Falling back due to: " + ", ".join(reasons) - return "Fallback status: No fallback required" + return ( + f"[{self._backend}] Fallback status: Falling back due to: " + + ", ".join(reasons) + ) + return f"[{self._backend}] Fallback status: No fallback required" class VideoDecoder: @@ -241,6 +245,11 @@ def cpu_fallback(self) -> CpuFallbackStatus: if "status unknown" not in backend_details: self._cpu_fallback.status_known = True + for backend in ("FFmpeg CUDA", "Beta CUDA", "CPU"): + if backend_details.startswith(backend): + self._cpu_fallback._backend = backend + break + if "CPU fallback" in backend_details: if "NVCUVID not available" in backend_details: self._cpu_fallback._nvcuvid_unavailable = True diff --git a/test/test_decoders.py b/test/test_decoders.py index 95034b259..b56d70290 100644 --- a/test/test_decoders.py +++ b/test/test_decoders.py @@ -1672,22 +1672,27 @@ def test_beta_cuda_interface_cpu_fallback(self): # to the CPU path, too. ref_dec = VideoDecoder(H265_VIDEO.path, device="cuda") - ref_frames = ref_dec.get_frame_at(0) - assert ( - _core._get_backend_details(ref_dec._decoder) - == "FFmpeg CUDA Device Interface. Using CPU fallback." - ) + + # Before accessing any frames, status should be unknown + assert not ref_dec.cpu_fallback.status_known + + ref_frame = ref_dec.get_frame_at(0) + + assert "FFmpeg CUDA" in str(ref_dec.cpu_fallback) + assert ref_dec.cpu_fallback.status_known + assert bool(ref_dec.cpu_fallback) with set_cuda_backend("beta"): beta_dec = VideoDecoder(H265_VIDEO.path, device="cuda") - assert ( - _core._get_backend_details(beta_dec._decoder) - == "Beta CUDA Device Interface. Using CPU fallback." - ) + assert "Beta CUDA" in str(beta_dec.cpu_fallback) + # For beta interface, status is known immediately + assert beta_dec.cpu_fallback.status_known + assert bool(beta_dec.cpu_fallback) + beta_frame = beta_dec.get_frame_at(0) - assert psnr(ref_frames.data, beta_frame.data) > 25 + assert psnr(ref_frame.data, beta_frame.data) > 25 @needs_cuda def test_beta_cuda_interface_error(self): @@ -1715,7 +1720,8 @@ def test_set_cuda_backend(self): # Check that the default is the ffmpeg backend assert _get_cuda_backend() == "ffmpeg" dec = VideoDecoder(H265_VIDEO.path, device="cuda") - assert _core._get_backend_details(dec._decoder).startswith("FFmpeg CUDA") + _ = dec.get_frame_at(0) + assert "FFmpeg CUDA" in str(dec.cpu_fallback) # Check the setting "beta" effectively uses the BETA backend. # We also show that the affects decoder creation only. When the decoder @@ -1724,9 +1730,9 @@ def test_set_cuda_backend(self): with set_cuda_backend("beta"): dec = VideoDecoder(H265_VIDEO.path, device="cuda") assert _get_cuda_backend() == "ffmpeg" - assert _core._get_backend_details(dec._decoder).startswith("Beta CUDA") + assert "Beta CUDA" in str(dec.cpu_fallback) with set_cuda_backend("ffmpeg"): - assert _core._get_backend_details(dec._decoder).startswith("Beta CUDA") + assert "Beta CUDA" in str(dec.cpu_fallback) # 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 @@ -1762,23 +1768,6 @@ def test_cpu_fallback_h265_video_ffmpeg_cuda(self): assert bool(decoder.cpu_fallback) assert "Fallback status: Falling back due to:" in str(decoder.cpu_fallback) - @needs_cuda - def test_cpu_fallback_h265_video_beta_cuda(self): - """Test that H265 video triggers CPU fallback on Beta CUDA interface.""" - with set_cuda_backend("beta"): - decoder = VideoDecoder(H265_VIDEO.path, device="cuda") - - # Before accessing any frames, status should be unknown - assert decoder.cpu_fallback.status_known - - _ = decoder.get_frame_at(0) - - # After accessing frames, status should be known - assert decoder.cpu_fallback.status_known - - assert bool(decoder.cpu_fallback) - assert "Fallback status: Falling back due to:" in str(decoder.cpu_fallback) - @needs_cuda def test_cpu_fallback_no_fallback_on_supported_video(self): """Test that supported videos don't trigger fallback on CUDA."""