Skip to content

Commit 20aa030

Browse files
committed
initial draft of to_dtype_cvcuda
1 parent e14e210 commit 20aa030

File tree

3 files changed

+153
-1
lines changed

3 files changed

+153
-1
lines changed

test/test_transforms_v2.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2656,7 +2656,8 @@ def test_transform(self, make_input, input_dtype, output_dtype, device, scale, a
26562656
output_dtype = {type(input): output_dtype}
26572657
check_transform(transforms.ToDtype(dtype=output_dtype, scale=scale), input, check_sample_input=not as_dict)
26582658

2659-
def reference_convert_dtype_image_tensor(self, image, dtype=torch.float, scale=False):
2659+
@staticmethod
2660+
def reference_convert_dtype_image_tensor(image, dtype=torch.float, scale=False):
26602661
input_dtype = image.dtype
26612662
output_dtype = dtype
26622663

@@ -2807,6 +2808,91 @@ def test_uint16(self):
28072808
assert_close(F.to_dtype(img_uint8, torch.float32, scale=True), img_float32, rtol=0, atol=1e-2)
28082809

28092810

2811+
@pytest.mark.skipif(not CVCUDA_AVAILABLE, reason="cvcuda is not available")
2812+
@needs_cuda
2813+
class TestToDtypeCVCUDA:
2814+
@pytest.mark.parametrize("input_dtype", [torch.float32, torch.float64, torch.uint8])
2815+
@pytest.mark.parametrize("output_dtype", [torch.float32, torch.float64, torch.uint8])
2816+
@pytest.mark.parametrize("device", cpu_and_cuda())
2817+
@pytest.mark.parametrize("scale", (True, False))
2818+
def test_functional(self, input_dtype, output_dtype, device, scale):
2819+
check_functional(
2820+
F.to_dtype,
2821+
make_image_cvcuda(batch_dims=(1,), dtype=input_dtype, device=device),
2822+
dtype=output_dtype,
2823+
scale=scale,
2824+
)
2825+
2826+
@pytest.mark.parametrize("input_dtype", [torch.float32, torch.float64, torch.uint8])
2827+
@pytest.mark.parametrize("output_dtype", [torch.float32, torch.float64, torch.uint8])
2828+
@pytest.mark.parametrize("device", cpu_and_cuda())
2829+
@pytest.mark.parametrize("scale", (True, False))
2830+
@pytest.mark.parametrize("as_dict", (True, False))
2831+
def test_transform(self, input_dtype, output_dtype, device, scale, as_dict):
2832+
cvc_input = make_image_cvcuda(batch_dims=(1,), dtype=input_dtype, device=device)
2833+
if as_dict:
2834+
output_dtype = {type(cvc_input): output_dtype}
2835+
check_transform(transforms.ToDtype(dtype=output_dtype, scale=scale), cvc_input, check_sample_input=not as_dict)
2836+
2837+
@pytest.mark.parametrize("input_dtype", [torch.float32, torch.float64, torch.uint8, torch.uint16])
2838+
@pytest.mark.parametrize("output_dtype", [torch.float32, torch.float64, torch.uint8, torch.uint16])
2839+
@pytest.mark.parametrize("device", cpu_and_cuda())
2840+
@pytest.mark.parametrize("scale", (True, False))
2841+
def test_image_correctness(self, input_dtype, output_dtype, device, scale):
2842+
if input_dtype.is_floating_point and output_dtype == torch.int64:
2843+
pytest.xfail("float to int64 conversion is not supported")
2844+
if input_dtype == torch.uint8 and output_dtype == torch.uint16 and device == "cuda":
2845+
pytest.xfail("uint8 to uint16 conversion is not supported on cuda")
2846+
if input_dtype == torch.uint8 and output_dtype == torch.uint16 and scale:
2847+
pytest.xfail("uint8 to uint16 conversion with scale is not supported in F.to_dtype_image")
2848+
2849+
cvc_input = make_image_cvcuda(batch_dims=(1,), dtype=input_dtype, device=device)
2850+
torch_input = F.cvcuda_to_tensor(cvc_input)
2851+
2852+
out = F.to_dtype(cvc_input, dtype=output_dtype, scale=scale)
2853+
out = F.cvcuda_to_tensor(out)
2854+
2855+
expected = F.to_dtype(torch_input, dtype=output_dtype, scale=scale)
2856+
2857+
# there are some differences in dtype conversion between torchvision and cvcuda
2858+
# due to different rounding behavior when converting between types with different bit widths
2859+
# Check if we're converting to a type with more bits (without scaling)
2860+
in_bits = torch.iinfo(input_dtype).bits if not input_dtype.is_floating_point else None
2861+
out_bits = torch.iinfo(output_dtype).bits if not output_dtype.is_floating_point else None
2862+
2863+
if scale:
2864+
if input_dtype.is_floating_point and not output_dtype.is_floating_point:
2865+
# float -> int with scaling: allow for rounding differences
2866+
torch.testing.assert_close(out, expected, atol=1, rtol=0)
2867+
elif input_dtype == torch.uint16 and output_dtype == torch.uint8:
2868+
# uint16 -> uint8 with scaling: allow large differences
2869+
torch.testing.assert_close(out, expected, atol=255, rtol=0)
2870+
else:
2871+
torch.testing.assert_close(out, expected)
2872+
else:
2873+
if in_bits is not None and out_bits is not None and out_bits > in_bits:
2874+
# uint to larger uint without scaling: allow large differences due to bit expansion
2875+
if input_dtype == torch.uint8 and output_dtype == torch.uint16:
2876+
torch.testing.assert_close(out, expected, atol=255, rtol=0)
2877+
else:
2878+
torch.testing.assert_close(out, expected, atol=1, rtol=0)
2879+
elif not input_dtype.is_floating_point and not output_dtype.is_floating_point:
2880+
# uint to uint without scaling (same or smaller bits): allow for rounding
2881+
if input_dtype == torch.uint16 and output_dtype == torch.uint8:
2882+
# uint16 -> uint8 can have large differences due to bit reduction
2883+
torch.testing.assert_close(out, expected, atol=255, rtol=0)
2884+
else:
2885+
torch.testing.assert_close(out, expected)
2886+
elif input_dtype.is_floating_point and not output_dtype.is_floating_point:
2887+
# float -> uint without scaling: allow for rounding differences
2888+
torch.testing.assert_close(out, expected, atol=1, rtol=0)
2889+
elif not input_dtype.is_floating_point and output_dtype.is_floating_point:
2890+
# uint -> float without scaling: allow for rounding differences
2891+
torch.testing.assert_close(out, expected, atol=1, rtol=0)
2892+
else:
2893+
torch.testing.assert_close(out, expected)
2894+
2895+
28102896
class TestAdjustBrightness:
28112897
_CORRECTNESS_BRIGHTNESS_FACTORS = [0.5, 0.0, 1.0, 5.0]
28122898
_DEFAULT_BRIGHTNESS_FACTOR = _CORRECTNESS_BRIGHTNESS_FACTORS[0]

torchvision/transforms/v2/functional/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
sanitize_bounding_boxes,
159159
sanitize_keypoints,
160160
to_dtype,
161+
to_dtype_cvcuda,
161162
to_dtype_image,
162163
to_dtype_video,
163164
)

torchvision/transforms/v2/functional/_misc.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,71 @@ def _to_dtype_tensor_dispatch(inpt: torch.Tensor, dtype: torch.dtype, scale: boo
347347
return inpt.to(dtype)
348348

349349

350+
# cvcuda is only used if it is installed, so we can simply define empty mappings
351+
_torch_to_cvcuda_dtypes = {}
352+
_cvcuda_to_torch_dtypes = {}
353+
if CVCUDA_AVAILABLE:
354+
# put the entire conversion set here
355+
# only a subset are used for torchvision
356+
_torch_to_cvcuda_dtypes = {
357+
torch.uint8: cvcuda.Type.U8,
358+
torch.uint16: cvcuda.Type.U16,
359+
torch.uint32: cvcuda.Type.U32,
360+
torch.uint64: cvcuda.Type.U64,
361+
torch.int8: cvcuda.Type.S8,
362+
torch.int16: cvcuda.Type.S16,
363+
torch.int32: cvcuda.Type.S32,
364+
torch.int64: cvcuda.Type.S64,
365+
torch.float32: cvcuda.Type.F32,
366+
torch.float64: cvcuda.Type.F64,
367+
torch.complex64: cvcuda.Type.C64,
368+
torch.complex128: cvcuda.Type.C128,
369+
}
370+
# create reverse mapping
371+
_cvcuda_to_torch_dtypes = {v: k for k, v in _torch_to_cvcuda_dtypes.items()}
372+
373+
374+
def to_dtype_cvcuda(
375+
inpt: "cvcuda.Tensor",
376+
dtype: torch.dtype,
377+
scale: bool = False,
378+
) -> "cvcuda.Tensor":
379+
dtype_in = _cvcuda_to_torch_dtypes[inpt.dtype]
380+
cvc_dtype = _torch_to_cvcuda_dtypes[dtype]
381+
382+
if not scale:
383+
return cvcuda.convertto(inpt, dtype=cvc_dtype)
384+
385+
scale_val, offset = 1.0, 0.0
386+
in_dtype_float = dtype_in.is_floating_point
387+
out_dtype_float = dtype.is_floating_point
388+
389+
# four cases for the scaling setup
390+
# 1. float -> float
391+
# 2. int -> int
392+
# 3. float -> int
393+
# 4. int -> float
394+
if in_dtype_float and out_dtype_float:
395+
scale_val, offset = 1.0, 0.0
396+
elif not in_dtype_float and not out_dtype_float:
397+
scale_val, offset = 1.0, 0.0
398+
elif in_dtype_float and not out_dtype_float:
399+
scale_val, offset = float(_max_value(dtype)), 0.0
400+
else:
401+
scale_val, offset = 1.0 / float(_max_value(dtype_in)), 0.0
402+
403+
return cvcuda.convertto(
404+
inpt,
405+
dtype=cvc_dtype,
406+
scale=scale_val,
407+
offset=offset,
408+
)
409+
410+
411+
if CVCUDA_AVAILABLE:
412+
_register_kernel_internal(to_dtype, cvcuda.Tensor)(to_dtype_cvcuda)
413+
414+
350415
def sanitize_bounding_boxes(
351416
bounding_boxes: torch.Tensor,
352417
format: Optional[tv_tensors.BoundingBoxFormat] = None,

0 commit comments

Comments
 (0)