Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/torchcodec/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# but that results in circular import.
from ._frame import AudioSamples, Frame, FrameBatch # usort:skip # noqa
from . import decoders, samplers # noqa
from .decoders._decoder_utils import set_cuda_backend # noqa

try:
# Note that version.py is generated during install.
Expand Down
53 changes: 52 additions & 1 deletion src/torchcodec/decoders/_decoder_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

import contextvars
import io
from contextlib import contextmanager
from pathlib import Path

from typing import Union
from typing import Generator, Union

from torch import Tensor
from torchcodec import _core as core
Expand Down Expand Up @@ -50,3 +52,52 @@ def create_decoder(
"read(self, size: int) -> bytes and "
"seek(self, offset: int, whence: int) -> int methods."
)


# Thread-local and async-safe storage for the current CUDA backend
_CUDA_BACKEND: contextvars.ContextVar[str] = contextvars.ContextVar(
"_CUDA_BACKEND", default="ffmpeg"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we plan on changing the default to be the beta interface at some point in the future? Would we need to indicate the change to users in some way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we'll want the current "beta" to eventually become the default interface. Usually the way to give notice is that for a few versions, we'll be raising a warning if the user doesn't explicitly requests either the "beta" or the "ffmpeg" interface. Basically we'd force them to use the CM, or they get a warning. Then we do the switch and remove the warning.

This is a bit disruptive and annoying, maybe we'll decide that the new interface is just strictly better and 99.9999% compatible with the existing one, at which point we could decide not to raise any warning and just silently do the switch.



@contextmanager
def set_cuda_backend(backend: str) -> Generator[None, None, None]:
"""Context Manager to set the CUDA backend for :class:`~torchcodec.decoders.VideoDecoder`.

This context manager allows you to specify which CUDA backend implementation
to use when creating :class:`~torchcodec.decoders.VideoDecoder` instances
with CUDA devices. This is thread-safe and async-safe.

Note that you still need to pass `device="cuda"` when creating the
:class:`~torchcodec.decoders.VideoDecoder` instance. If a CUDA device isn't
specified, this context manager will have no effect.

Only the creation of the decoder needs to be inside the context manager, the
decoding methods can be called outside of it.

Args:
backend (str): The CUDA backend to use. Can be "ffmpeg" or "beta". Default is "ffmpeg".

Example:
>>> with torchcodec.set_cuda_backend("beta"):
... decoder = VideoDecoder("video.mp4", device="cuda")
...
... # Only the decoder creation needs to be part of the context manager.
... # Decoder will now the beta CUDA implementation:
... decoder.get_frame_at(0)
"""
backend = backend.lower()
if backend not in ("ffmpeg", "beta"):
raise ValueError(
f"Invalid CUDA backend ({backend}). Supported values are 'ffmpeg' and 'beta'."
)

previous_state = _CUDA_BACKEND.set(backend)
try:
yield
finally:
_CUDA_BACKEND.reset(previous_state)


def _get_current_cuda_backend() -> str:
return _CUDA_BACKEND.get()
23 changes: 12 additions & 11 deletions src/torchcodec/decoders/_video_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from torchcodec import _core as core, Frame, FrameBatch
from torchcodec.decoders._decoder_utils import (
_get_current_cuda_backend,
create_decoder,
ERROR_REPORTING_INSTRUCTIONS,
)
Expand Down Expand Up @@ -143,17 +144,17 @@ def __init__(
if isinstance(device, torch_device):
device = str(device)

# If device looks like "cuda:0:beta", make it "cuda:0" and set
# device_variant to "beta"
# TODONVDEC P2 Consider alternative ways of exposing custom device
# variants, and if we want this new decoder backend to be a "device
# variant" at all.
device_variant = "default"
if device is not None:
device_split = device.split(":")
if len(device_split) == 3:
device_variant = device_split[2]
device = ":".join(device_split[0:2])
device_variant = _get_current_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
# updating our tests.
if device == "cuda:0:beta":
device = "cuda:0"
device_variant = "beta"

core.add_video_stream(
self._decoder,
Expand Down
2 changes: 1 addition & 1 deletion test/test_decoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -1702,7 +1702,7 @@ def test_beta_cuda_interface_small_h265(self):

@needs_cuda
def test_beta_cuda_interface_error(self):
with pytest.raises(RuntimeError, match="Unsupported device"):
with pytest.raises(RuntimeError, match="Invalid device string"):
VideoDecoder(NASA_VIDEO.path, device="cuda:0:bad_variant")


Expand Down
Loading