From b1cb442d58e624afbc33ab67f12e91f4a22ff588 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 14 Sep 2021 20:57:37 +0000 Subject: [PATCH 1/6] Add dtype to ToCuPy Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/utility/array.py | 13 ++++++++++--- monai/transforms/utility/dictionary.py | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index e53f0e1fe3..b1a983432c 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -393,15 +393,22 @@ def __call__(self, img: NdarrayOrTensor) -> np.ndarray: class ToCupy(Transform): """ Converts the input data to CuPy array, can support list or tuple of numbers, NumPy and PyTorch Tensor. + + Args: + dtype: data type specifier. It is inferred from the input by default. """ backend = [TransformBackends.TORCH, TransformBackends.NUMPY] - def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: + def __init__(self, dtype=None) -> None: + super().__init__() + self.dtype = dtype + + def __call__(self, img: NdarrayOrTensor): """ - Apply the transform to `img` and make it contiguous. + Create a CuPy array from `img` and make it contiguous """ - return cp.ascontiguousarray(cp.asarray(img)) # type: ignore + return cp.ascontiguousarray(cp.asarray(img, dtype=self.dtype)) # type: ignore class ToPIL(Transform): diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index 2fdf20c1fe..77eae06d3e 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -553,7 +553,7 @@ class ToCupyd(MapTransform): backend = ToCupy.backend - def __init__(self, keys: KeysCollection, allow_missing_keys: bool = False) -> None: + def __init__(self, keys: KeysCollection, allow_missing_keys: bool = False, dtype=None) -> None: """ Args: keys: keys of the corresponding items to be transformed. @@ -561,7 +561,7 @@ def __init__(self, keys: KeysCollection, allow_missing_keys: bool = False) -> No allow_missing_keys: don't raise exception if key is missing. """ super().__init__(keys, allow_missing_keys) - self.converter = ToCupy() + self.converter = ToCupy(dtype=dtype) def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: d = dict(data) From abb175639f66ac457db7f2c8d3d359d11ac007cc Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 14 Sep 2021 21:16:02 +0000 Subject: [PATCH 2/6] Add unittests to include dtype Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_to_cupy.py | 54 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/tests/test_to_cupy.py b/tests/test_to_cupy.py index 8b00e12539..0fd9607339 100644 --- a/tests/test_to_cupy.py +++ b/tests/test_to_cupy.py @@ -22,49 +22,81 @@ cp, has_cp = optional_import("cupy") +@skipUnless(has_cp, "CuPy is required.") class TestToCupy(unittest.TestCase): - @skipUnless(has_cp, "CuPy is required.") def test_cupy_input(self): - test_data = cp.array([[1, 2], [3, 4]]) + test_data = cp.array([[1, 2], [3, 4]], dtype=cp.float32) test_data = cp.rot90(test_data) self.assertFalse(test_data.flags["C_CONTIGUOUS"]) result = ToCupy()(test_data) + self.assertTrue(result.dtype == cp.float32) + self.assertTrue(isinstance(result, cp.ndarray)) + self.assertTrue(result.flags["C_CONTIGUOUS"]) + cp.testing.assert_allclose(result, test_data) + + def test_cupy_input_dtype(self): + test_data = cp.array([[1, 2], [3, 4]], dtype=cp.float32) + test_data = cp.rot90(test_data) + self.assertFalse(test_data.flags["C_CONTIGUOUS"]) + result = ToCupy(cp.uint8)(test_data) + self.assertTrue(result.dtype == cp.uint8) self.assertTrue(isinstance(result, cp.ndarray)) self.assertTrue(result.flags["C_CONTIGUOUS"]) cp.testing.assert_allclose(result, test_data) - @skipUnless(has_cp, "CuPy is required.") def test_numpy_input(self): - test_data = np.array([[1, 2], [3, 4]]) + test_data = np.array([[1, 2], [3, 4]], dtype=np.float32) test_data = np.rot90(test_data) self.assertFalse(test_data.flags["C_CONTIGUOUS"]) result = ToCupy()(test_data) + self.assertTrue(result.dtype == cp.float32) + self.assertTrue(isinstance(result, cp.ndarray)) + self.assertTrue(result.flags["C_CONTIGUOUS"]) + cp.testing.assert_allclose(result, test_data) + + def test_numpy_input_dtype(self): + test_data = np.array([[1, 2], [3, 4]], dtype=np.float32) + test_data = np.rot90(test_data) + self.assertFalse(test_data.flags["C_CONTIGUOUS"]) + result = ToCupy(np.uint8)(test_data) + self.assertTrue(result.dtype == cp.uint8) self.assertTrue(isinstance(result, cp.ndarray)) self.assertTrue(result.flags["C_CONTIGUOUS"]) cp.testing.assert_allclose(result, test_data) - @skipUnless(has_cp, "CuPy is required.") def test_tensor_input(self): - test_data = torch.tensor([[1, 2], [3, 4]]) + test_data = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32) test_data = test_data.rot90() self.assertFalse(test_data.is_contiguous()) result = ToCupy()(test_data) + self.assertTrue(result.dtype == cp.float32) self.assertTrue(isinstance(result, cp.ndarray)) self.assertTrue(result.flags["C_CONTIGUOUS"]) - cp.testing.assert_allclose(result, test_data.numpy()) + cp.testing.assert_allclose(result, test_data) - @skipUnless(has_cp, "CuPy is required.") @skip_if_no_cuda def test_tensor_cuda_input(self): - test_data = torch.tensor([[1, 2], [3, 4]]).cuda() + test_data = torch.tensor([[1, 2], [3, 4]], dtype=torch.float32).cuda() test_data = test_data.rot90() self.assertFalse(test_data.is_contiguous()) result = ToCupy()(test_data) + self.assertTrue(result.dtype == cp.float32) self.assertTrue(isinstance(result, cp.ndarray)) self.assertTrue(result.flags["C_CONTIGUOUS"]) - cp.testing.assert_allclose(result, test_data.cpu().numpy()) + cp.testing.assert_allclose(result, test_data) + + @skip_if_no_cuda + def test_tensor_cuda_input_dtype(self): + test_data = torch.tensor([[1, 2], [3, 4]], dtype=torch.uint8).cuda() + test_data = test_data.rot90() + self.assertFalse(test_data.is_contiguous()) + + result = ToCupy(dtype="float32")(test_data) + self.assertTrue(result.dtype == cp.float32) + self.assertTrue(isinstance(result, cp.ndarray)) + self.assertTrue(result.flags["C_CONTIGUOUS"]) + cp.testing.assert_allclose(result, test_data) - @skipUnless(has_cp, "CuPy is required.") def test_list_tuple(self): test_data = [[1, 2], [3, 4]] result = ToCupy()(test_data) From c77623eb6245411df8e9207af6392d0c322330ed Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 15 Sep 2021 00:36:10 +0000 Subject: [PATCH 3/6] Implement convert_to_cupy Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/utility/array.py | 16 +++++++--- monai/utils/__init__.py | 1 + monai/utils/type_conversion.py | 51 +++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index b1a983432c..4bc09b59a5 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -32,7 +32,15 @@ map_classes_to_indices, ) from monai.transforms.utils_pytorch_numpy_unification import in1d, moveaxis -from monai.utils import convert_to_numpy, convert_to_tensor, ensure_tuple, look_up_option, min_version, optional_import +from monai.utils import ( + convert_to_cupy, + convert_to_numpy, + convert_to_tensor, + ensure_tuple, + look_up_option, + min_version, + optional_import, +) from monai.utils.enums import TransformBackends from monai.utils.misc import is_module_ver_at_least from monai.utils.type_conversion import convert_data_type @@ -404,11 +412,11 @@ def __init__(self, dtype=None) -> None: super().__init__() self.dtype = dtype - def __call__(self, img: NdarrayOrTensor): + def __call__(self, data: NdarrayOrTensor): """ - Create a CuPy array from `img` and make it contiguous + Create a CuPy array from `data` and make it contiguous """ - return cp.ascontiguousarray(cp.asarray(img, dtype=self.dtype)) # type: ignore + return convert_to_cupy(data, self.dtype) class ToPIL(Transform): diff --git a/monai/utils/__init__.py b/monai/utils/__init__.py index aa8f02f815..dc3922933d 100644 --- a/monai/utils/__init__.py +++ b/monai/utils/__init__.py @@ -77,6 +77,7 @@ from .state_cacher import StateCacher from .type_conversion import ( convert_data_type, + convert_to_cupy, convert_to_dst_type, convert_to_numpy, convert_to_tensor, diff --git a/monai/utils/type_conversion.py b/monai/utils/type_conversion.py index b51ff6a9c8..46f794d20e 100644 --- a/monai/utils/type_conversion.py +++ b/monai/utils/type_conversion.py @@ -16,6 +16,7 @@ "get_equivalent_dtype", "convert_data_type", "get_dtype", + "convert_to_cupy", "convert_to_numpy", "convert_to_tensor", "convert_to_dst_type", @@ -154,6 +155,49 @@ def convert_to_numpy(data, wrap_sequence: bool = False): return data +def convert_to_cupy(data, dtype, wrap_sequence: bool = True): + """ + Utility to convert the input data to a cupy array. If passing a dictionary, list or tuple, + recursively check every item and convert it to cupy array. + + Args: + data: input data can be PyTorch Tensor, numpy array, cupy array, list, dictionary, int, float, bool, str, etc. + Tensor, numpy array, cupy array, float, int, bool are converted to cupy arrays + + for dictionary, list or tuple, convert every item to a numpy array if applicable. + wrap_sequence: if `False`, then lists will recursively call this function. E.g., `[1, 2]` -> `[array(1), array(2)]`. + If `True`, then `[1, 2]` -> `array([1, 2])`. + """ + + # direct calls + if isinstance(data, cp_ndarray): + if dtype is not None: + data = data.astype(dtype) + elif isinstance(data, np.ndarray): + data = cp.asarray(data, dtype) + elif isinstance(data, torch.Tensor): + data = cp.asarray(data, dtype) + elif isinstance(data, (float, int, bool)): + data = cp.asarray(data, dtype) + # recursive calls + elif isinstance(data, Sequence) and wrap_sequence: + return cp.asarray(data) + elif isinstance(data, list): + return [convert_to_cupy(i, dtype) for i in data] + elif isinstance(data, tuple): + return tuple(convert_to_cupy(i, dtype) for i in data) + elif isinstance(data, dict): + return {k: convert_to_cupy(v, dtype) for k, v in data.items()} + # make it contiguous + if isinstance(data, cp.ndarray): + if data.ndim > 0: + data = cp.ascontiguousarray(data) + else: + raise ValueError(f"The input data type [{type(data)}] cannot be converted into cupy arrays!") + + return data + + def convert_data_type( data: Any, output_type: Optional[type] = None, @@ -178,6 +222,8 @@ def convert_data_type( orig_type = torch.Tensor elif isinstance(data, np.ndarray): orig_type = np.ndarray + elif has_cp and isinstance(data, cp.ndarray): + orig_type = cp.ndarray else: orig_type = type(data) @@ -199,6 +245,11 @@ def convert_data_type( data = convert_to_numpy(data) if data is not None and dtype != data.dtype: data = data.astype(dtype) + elif has_cp and output_type is cp.ndarray: + if orig_type is not cp.ndarray: + data = convert_to_cupy(data, dtype) + elif data is not None and dtype != data.dtype: + data = data.astype(dtype) else: raise ValueError(f"Unsupported output type: {output_type}") return data, orig_type, orig_device From a15d06ce7a081b4fb109da93a6857252d091b793 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 15 Sep 2021 04:06:21 +0000 Subject: [PATCH 4/6] Addressed all comments Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/utility/dictionary.py | 12 ++++++------ monai/utils/type_conversion.py | 11 ++--------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index 77eae06d3e..4bc8ff1afa 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -549,17 +549,17 @@ def __call__(self, data: Mapping[Hashable, Any]) -> Dict[Hashable, Any]: class ToCupyd(MapTransform): """ Dictionary-based wrapper of :py:class:`monai.transforms.ToCupy`. + + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + allow_missing_keys: don't raise exception if key is missing. + dtype: data type specifier. It is inferred from the input by default. """ backend = ToCupy.backend def __init__(self, keys: KeysCollection, allow_missing_keys: bool = False, dtype=None) -> None: - """ - Args: - keys: keys of the corresponding items to be transformed. - See also: :py:class:`monai.transforms.compose.MapTransform` - allow_missing_keys: don't raise exception if key is missing. - """ super().__init__(keys, allow_missing_keys) self.converter = ToCupy(dtype=dtype) diff --git a/monai/utils/type_conversion.py b/monai/utils/type_conversion.py index 46f794d20e..ef9038bf16 100644 --- a/monai/utils/type_conversion.py +++ b/monai/utils/type_conversion.py @@ -170,14 +170,7 @@ def convert_to_cupy(data, dtype, wrap_sequence: bool = True): """ # direct calls - if isinstance(data, cp_ndarray): - if dtype is not None: - data = data.astype(dtype) - elif isinstance(data, np.ndarray): - data = cp.asarray(data, dtype) - elif isinstance(data, torch.Tensor): - data = cp.asarray(data, dtype) - elif isinstance(data, (float, int, bool)): + if isinstance(data, (cp_ndarray, np.ndarray, torch.Tensor, float, int, bool)): data = cp.asarray(data, dtype) # recursive calls elif isinstance(data, Sequence) and wrap_sequence: @@ -248,7 +241,7 @@ def convert_data_type( elif has_cp and output_type is cp.ndarray: if orig_type is not cp.ndarray: data = convert_to_cupy(data, dtype) - elif data is not None and dtype != data.dtype: + elif data is not None: data = data.astype(dtype) else: raise ValueError(f"Unsupported output type: {output_type}") From 5f690ee2c1c43f136667ad5dfa4763211e82cbc7 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 15 Sep 2021 04:08:17 +0000 Subject: [PATCH 5/6] Addressed all comments Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/utils/type_conversion.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/monai/utils/type_conversion.py b/monai/utils/type_conversion.py index ef9038bf16..82a1c8cf25 100644 --- a/monai/utils/type_conversion.py +++ b/monai/utils/type_conversion.py @@ -239,10 +239,9 @@ def convert_data_type( if data is not None and dtype != data.dtype: data = data.astype(dtype) elif has_cp and output_type is cp.ndarray: - if orig_type is not cp.ndarray: + if data is not None: data = convert_to_cupy(data, dtype) - elif data is not None: - data = data.astype(dtype) + else: raise ValueError(f"Unsupported output type: {output_type}") return data, orig_type, orig_device From 0354c11de599f8a320d43ddf7a97e6c2e39c391d Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 15 Sep 2021 04:18:55 +0000 Subject: [PATCH 6/6] Add dtype for Sequence Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/utils/type_conversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/utils/type_conversion.py b/monai/utils/type_conversion.py index 82a1c8cf25..8a54986633 100644 --- a/monai/utils/type_conversion.py +++ b/monai/utils/type_conversion.py @@ -174,7 +174,7 @@ def convert_to_cupy(data, dtype, wrap_sequence: bool = True): data = cp.asarray(data, dtype) # recursive calls elif isinstance(data, Sequence) and wrap_sequence: - return cp.asarray(data) + return cp.asarray(data, dtype) elif isinstance(data, list): return [convert_to_cupy(i, dtype) for i in data] elif isinstance(data, tuple):