Skip to content

Commit 7b7b532

Browse files
authored
Let device default be None on blocks (#1656)
1 parent 20cc890 commit 7b7b532

6 files changed

Lines changed: 61 additions & 12 deletions

File tree

examples/decoding/blocks.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
#
6666
# ``PacketDecoder`` and ``ColorConverter`` both accept ``device="cuda"``:
6767
# decoding then runs on NVDEC and the color conversion on the GPU, and the
68-
# frames never leave the device. Demuxing always happens on the CPU.
68+
# frames never leave the device. Demuxing always happens on the CPU. Left
69+
# unspecified, ``device`` is the current default device.
6970
from torchcodec.decoders._blocks import ColorConverter, Demuxer, PacketDecoder
7071

7172
demuxer = Demuxer(video_path)

src/torchcodec/decoders/_blocks/_color_converter.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from torchcodec._core.ops import _blocks_convert_frame, _blocks_create_color_converter
1414
from torchcodec._frame import Frame
1515

16-
from .._decoder_utils import convert_output_dtype_to_str
16+
from .._decoder_utils import convert_device_to_str, convert_output_dtype_to_str
1717
from ._frame import DecodedFrame
1818

1919

@@ -35,16 +35,19 @@ class ColorConverter:
3535
Rotation is applied too, so the output matches ``VideoDecoder``'s. The angle
3636
is part of the frame, like its dims and colorspace, so honoring it doesn't
3737
bind the converter to a stream either.
38+
39+
``device`` accepts a string or a ``torch.device``. It defaults to ``None``,
40+
which means the current default device (see ``torch.set_default_device``).
3841
"""
3942

40-
# TODO_API_BREAKDOWN UF P1: device default should be None
4143
def __init__(
4244
self,
43-
device="cpu",
45+
device: str | torch.device | None = None,
4446
output_dtype: torch.dtype | Literal["auto"] = torch.uint8,
4547
):
4648
self._handle = _blocks_create_color_converter(
47-
device=device, output_dtype=convert_output_dtype_to_str(output_dtype)
49+
device=convert_device_to_str(device),
50+
output_dtype=convert_output_dtype_to_str(output_dtype),
4851
)
4952

5053
def convert(self, decoded_frame: DecodedFrame) -> Frame:

src/torchcodec/decoders/_blocks/_packet_decoder.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from __future__ import annotations
88

9+
import torch
10+
911
from torchcodec._core.ops import (
1012
_blocks_create_packet_decoder,
1113
_blocks_packet_decoder_receive_frame,
@@ -14,6 +16,7 @@
1416
_blocks_packet_decoder_send_packet,
1517
)
1618

19+
from .._decoder_utils import convert_device_to_str
1720
from ._demuxer import Demuxer
1821
from ._frame import DecodedFrame, Packet
1922

@@ -30,12 +33,14 @@ class PacketDecoder:
3033
use one ``PacketDecoder`` per thread. FFmpeg's internal codec thread count
3134
is kept at 1 for now (not exposed); parallelism comes from composing blocks
3235
on your own threads.
36+
37+
``device`` accepts a string or a ``torch.device``. It defaults to ``None``,
38+
which means the current default device (see ``torch.set_default_device``).
3339
"""
3440

35-
# TODO_API_BREAKDOWN UF P1: device default should be None, here and everywhere else
36-
def __init__(self, demuxer: Demuxer, device="cpu"):
41+
def __init__(self, demuxer: Demuxer, device: str | torch.device | None = None):
3742
self._handle = _blocks_create_packet_decoder(
38-
demuxer._handle, num_threads=1, device=device
43+
demuxer._handle, num_threads=1, device=convert_device_to_str(device)
3944
)
4045
self._drained = False
4146

src/torchcodec/decoders/_decoder_utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ def get_nvdec_cache_capacity() -> int:
107107
return _core.get_nvdec_cache_capacity()
108108

109109

110+
def convert_device_to_str(device: str | torch.device | None) -> str:
111+
# The core ops take the device as a string. None means "the current default
112+
# device".
113+
if device is None:
114+
device = torch.get_default_device()
115+
return str(device)
116+
117+
110118
def convert_output_dtype_to_str(output_dtype: torch.dtype | str) -> str:
111119
# The core ops take the dtype as a string.
112120
dtype_to_str = {torch.uint8: "uint8", torch.float32: "float32"}

src/torchcodec/decoders/_video_decoder.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from torchcodec._logging import _LG
2121
from torchcodec.decoders._decoder_utils import (
2222
_get_cuda_backend,
23+
convert_device_to_str,
2324
convert_output_dtype_to_str,
2425
)
2526
from torchcodec.transforms import DecoderTransform
@@ -227,10 +228,7 @@ def __init__(
227228
output_dtype = convert_output_dtype_to_str(output_dtype)
228229

229230
device_variant = _get_cuda_backend()
230-
if device is None:
231-
device = str(torch.get_default_device())
232-
elif isinstance(device, torch_device):
233-
device = str(device)
231+
device = convert_device_to_str(device)
234232

235233
if (
236234
device.startswith("cuda")

test/test_decoders.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3902,6 +3902,40 @@ def _first_frame(self, path, device):
39023902
frame = next(self._decode(decoder, self._demux(demuxer)))
39033903
return frame, converter
39043904

3905+
@pytest.mark.parametrize("device_str", _block_devices())
3906+
def test_device_none_default_device(self, device_str):
3907+
# PacketDecoder and ColorConverter default to device=None, which should
3908+
# respect both the torch.device() context manager and
3909+
# torch.set_default_device().
3910+
3911+
def assert_first_frame_is_on_default_device():
3912+
# Note the absence of any device parameter.
3913+
demuxer = Demuxer(NASA_VIDEO.path)
3914+
decoder = PacketDecoder(demuxer)
3915+
converter = ColorConverter()
3916+
decoded = next(self._decode(decoder, self._demux(demuxer)))
3917+
# DecodedFrame.device is a string, and it carries an index
3918+
# ("cuda:0") since that's what torch.get_default_device() reports.
3919+
assert torch.device(decoded.device).type == device_str
3920+
assert converter.convert(decoded).data.device.type == device_str
3921+
3922+
with torch.device(device_str):
3923+
assert_first_frame_is_on_default_device()
3924+
3925+
original_device = torch.get_default_device()
3926+
try:
3927+
torch.set_default_device(device_str)
3928+
assert_first_frame_is_on_default_device()
3929+
finally:
3930+
torch.set_default_device(original_device)
3931+
3932+
@pytest.mark.parametrize("device", _block_devices())
3933+
def test_device_torch_device_instance(self, device):
3934+
# device can be a torch.device instance, not just a string.
3935+
frame, converter = self._first_frame(NASA_VIDEO.path, torch.device(device))
3936+
assert torch.device(frame.device).type == device
3937+
assert converter.convert(frame).data.device.type == device
3938+
39053939
@pytest.mark.parametrize("case", _MATERIALIZE_VIDEOS, ids=_materialize_ids)
39063940
@pytest.mark.parametrize("device", _block_devices())
39073941
def test_materialize_structure(self, case, device):

0 commit comments

Comments
 (0)