From aa93e6c3a7873f79583897c9074ba522949d81d2 Mon Sep 17 00:00:00 2001 From: benediktjohannes Date: Sat, 17 Jan 2026 16:28:57 +0100 Subject: [PATCH 1/7] Fixes #8697 GPU memory leak by checking both image and label tensors for CUDA device Modified device detection to check BOTH image and label tensors torch.cuda.empty_cache() now called if EITHER tensor is on GPU Prevents GPU memory leaks in mixed device scenarios Signed-off-by: benediktjohannes --- monai/auto3dseg/analyzer.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/monai/auto3dseg/analyzer.py b/monai/auto3dseg/analyzer.py index 8d662df83d..a2d8c77e25 100644 --- a/monai/auto3dseg/analyzer.py +++ b/monai/auto3dseg/analyzer.py @@ -468,21 +468,18 @@ def __call__(self, data: Mapping[Hashable, MetaTensor]) -> dict[Hashable, MetaTe """ d: dict[Hashable, MetaTensor] = dict(data) start = time.time() - if isinstance(d[self.image_key], (torch.Tensor, MetaTensor)) and d[self.image_key].device.type == "cuda": - using_cuda = True - else: - using_cuda = False + using_cuda = any(isinstance(t, torch.Tensor) and t.device.type == "cuda" for t in (image_tensor, label_tensor)) restore_grad_state = torch.is_grad_enabled() torch.set_grad_enabled(False) - ndas: list[MetaTensor] = [d[self.image_key][i] for i in range(d[self.image_key].shape[0])] # type: ignore - ndas_label: MetaTensor = d[self.label_key].astype(torch.int16) # (H,W,D) + ndas: list[MetaTensor] = [image_tensor[i] for i in range(image_tensor.shape[0])] # type: ignore + ndas_label: MetaTensor = label_tensor.astype(torch.int16) # (H,W,D) if ndas_label.shape != ndas[0].shape: raise ValueError(f"Label shape {ndas_label.shape} is different from image shape {ndas[0].shape}") nda_foregrounds: list[torch.Tensor] = [get_foreground_label(nda, ndas_label) for nda in ndas] - nda_foregrounds = [nda if nda.numel() > 0 else torch.Tensor([0]) for nda in nda_foregrounds] + nda_foregrounds = [nda if nda.numel() > 0 else MetaTensor([0.0]) for nda in nda_foregrounds] unique_label = unique(ndas_label) if isinstance(ndas_label, (MetaTensor, torch.Tensor)): From f8cc9fff4268ad87a456fb4fe27483d5095e3ed3 Mon Sep 17 00:00:00 2001 From: benediktjohannes Date: Sat, 17 Jan 2026 16:47:10 +0100 Subject: [PATCH 2/7] Forgot to define variables, thanks coderabbitai for the information Signed-off-by: benediktjohannes --- monai/auto3dseg/analyzer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monai/auto3dseg/analyzer.py b/monai/auto3dseg/analyzer.py index a2d8c77e25..736607a815 100644 --- a/monai/auto3dseg/analyzer.py +++ b/monai/auto3dseg/analyzer.py @@ -468,6 +468,8 @@ def __call__(self, data: Mapping[Hashable, MetaTensor]) -> dict[Hashable, MetaTe """ d: dict[Hashable, MetaTensor] = dict(data) start = time.time() + image_tensor = d[self.image_key] + label_tensor = d[self.label_key] using_cuda = any(isinstance(t, torch.Tensor) and t.device.type == "cuda" for t in (image_tensor, label_tensor)) restore_grad_state = torch.is_grad_enabled() torch.set_grad_enabled(False) From c53253539b0f3d2f38f30efa7b34f87871e47363 Mon Sep 17 00:00:00 2001 From: benediktjohannes Date: Sat, 24 Jan 2026 01:40:46 +0100 Subject: [PATCH 3/7] Update analyzer.py Signed-off-by: benediktjohannes --- monai/auto3dseg/analyzer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/auto3dseg/analyzer.py b/monai/auto3dseg/analyzer.py index 736607a815..a0c217d32f 100644 --- a/monai/auto3dseg/analyzer.py +++ b/monai/auto3dseg/analyzer.py @@ -470,7 +470,7 @@ def __call__(self, data: Mapping[Hashable, MetaTensor]) -> dict[Hashable, MetaTe start = time.time() image_tensor = d[self.image_key] label_tensor = d[self.label_key] - using_cuda = any(isinstance(t, torch.Tensor) and t.device.type == "cuda" for t in (image_tensor, label_tensor)) + using_cuda = any(isinstance(t, (torch.Tensor, MetaTensor)) and t.device.type == "cuda" for t in (image_tensor, label_tensor)) restore_grad_state = torch.is_grad_enabled() torch.set_grad_enabled(False) From a963ac8839fc066260ae1854828711546ee7f159 Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sun, 22 Feb 2026 15:40:50 +0100 Subject: [PATCH 4/7] Update analyzer.py Signed-off-by: Benedikt Johannes --- monai/auto3dseg/analyzer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/monai/auto3dseg/analyzer.py b/monai/auto3dseg/analyzer.py index a0c217d32f..fbbf5d4f5c 100644 --- a/monai/auto3dseg/analyzer.py +++ b/monai/auto3dseg/analyzer.py @@ -470,7 +470,10 @@ def __call__(self, data: Mapping[Hashable, MetaTensor]) -> dict[Hashable, MetaTe start = time.time() image_tensor = d[self.image_key] label_tensor = d[self.label_key] - using_cuda = any(isinstance(t, (torch.Tensor, MetaTensor)) and t.device.type == "cuda" for t in (image_tensor, label_tensor)) + using_cuda = any( + isinstance(t, (torch.Tensor, MetaTensor)) and t.device.type == "cuda" + for t in (image_tensor, label_tensor) + ) restore_grad_state = torch.is_grad_enabled() torch.set_grad_enabled(False) From 2820618abfd4cf5354d3f8694542f914e38b3dfd Mon Sep 17 00:00:00 2001 From: Benedikt Johannes Date: Sun, 22 Feb 2026 15:47:00 +0100 Subject: [PATCH 5/7] Update analyzer.py Signed-off-by: Benedikt Johannes --- monai/auto3dseg/analyzer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/auto3dseg/analyzer.py b/monai/auto3dseg/analyzer.py index fbbf5d4f5c..8f84e2ebcc 100644 --- a/monai/auto3dseg/analyzer.py +++ b/monai/auto3dseg/analyzer.py @@ -471,8 +471,8 @@ def __call__(self, data: Mapping[Hashable, MetaTensor]) -> dict[Hashable, MetaTe image_tensor = d[self.image_key] label_tensor = d[self.label_key] using_cuda = any( - isinstance(t, (torch.Tensor, MetaTensor)) and t.device.type == "cuda" - for t in (image_tensor, label_tensor) + isinstance(t, (torch.Tensor, MetaTensor)) and t.device.type == "cuda" + for t in (image_tensor, label_tensor) ) restore_grad_state = torch.is_grad_enabled() torch.set_grad_enabled(False) From 9ba30447bf7826598e0749cc57db78d3b3cacc12 Mon Sep 17 00:00:00 2001 From: "R. Garcia-Dias" Date: Tue, 17 Mar 2026 15:52:54 +0000 Subject: [PATCH 6/7] autofix Signed-off-by: R. Garcia-Dias --- monai/apps/detection/transforms/box_ops.py | 2 +- monai/apps/detection/transforms/dictionary.py | 36 +++++++------------ monai/apps/detection/utils/anchor_utils.py | 18 ++++------ monai/apps/reconstruction/transforms/array.py | 6 ++-- monai/auto3dseg/analyzer.py | 5 ++- monai/bundle/scripts.py | 2 +- monai/bundle/utils.py | 6 ++-- monai/data/dataset.py | 4 +-- monai/handlers/utils.py | 2 +- monai/losses/dice.py | 6 ++-- monai/losses/focal_loss.py | 6 ++-- monai/metrics/utils.py | 2 +- monai/transforms/io/array.py | 1 + monai/transforms/utility/array.py | 2 +- tests/integration/test_loader_semaphore.py | 1 + tests/profile_subclass/profiling.py | 1 + tests/profile_subclass/pyspy_profiling.py | 1 + versioneer.py | 5 ++- 18 files changed, 41 insertions(+), 65 deletions(-) diff --git a/monai/apps/detection/transforms/box_ops.py b/monai/apps/detection/transforms/box_ops.py index 6e08a88e59..fa714daad1 100644 --- a/monai/apps/detection/transforms/box_ops.py +++ b/monai/apps/detection/transforms/box_ops.py @@ -267,7 +267,7 @@ def convert_box_to_mask( boxes_only_mask = np.ones(box_size, dtype=np.int16) * np.int16(labels_np[b]) # apply to global mask slicing = [b] - slicing.extend(slice(boxes_np[b, d], boxes_np[b, d + spatial_dims]) for d in range(spatial_dims)) # type:ignore + slicing.extend(slice(boxes_np[b, d], boxes_np[b, d + spatial_dims]) for d in range(spatial_dims)) # type: ignore boxes_mask_np[tuple(slicing)] = boxes_only_mask return convert_to_dst_type(src=boxes_mask_np, dst=boxes, dtype=torch.int16)[0] diff --git a/monai/apps/detection/transforms/dictionary.py b/monai/apps/detection/transforms/dictionary.py index 52b1a7d15d..9817a3b510 100644 --- a/monai/apps/detection/transforms/dictionary.py +++ b/monai/apps/detection/transforms/dictionary.py @@ -125,10 +125,8 @@ def __init__(self, box_keys: KeysCollection, box_ref_image_keys: str, allow_miss super().__init__(box_keys, allow_missing_keys) box_ref_image_keys_tuple = ensure_tuple(box_ref_image_keys) if len(box_ref_image_keys_tuple) > 1: - raise ValueError( - "Please provide a single key for box_ref_image_keys.\ - All boxes of box_keys are attached to box_ref_image_keys." - ) + raise ValueError("Please provide a single key for box_ref_image_keys.\ + All boxes of box_keys are attached to box_ref_image_keys.") self.box_ref_image_keys = box_ref_image_keys def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, NdarrayOrTensor]: @@ -289,10 +287,8 @@ def __init__( super().__init__(box_keys, allow_missing_keys) box_ref_image_keys_tuple = ensure_tuple(box_ref_image_keys) if len(box_ref_image_keys_tuple) > 1: - raise ValueError( - "Please provide a single key for box_ref_image_keys.\ - All boxes of box_keys are attached to box_ref_image_keys." - ) + raise ValueError("Please provide a single key for box_ref_image_keys.\ + All boxes of box_keys are attached to box_ref_image_keys.") self.box_ref_image_keys = box_ref_image_keys self.image_meta_key = image_meta_key or f"{box_ref_image_keys}_{image_meta_key_postfix}" self.converter_to_image_coordinate = AffineBox() @@ -310,10 +306,8 @@ def extract_affine(self, data: Mapping[Hashable, torch.Tensor]) -> tuple[Ndarray else: raise ValueError(f"{meta_key} is not found. Please check whether it is the correct the image meta key.") if "affine" not in meta_dict: - raise ValueError( - f"'affine' is not found in {meta_key}. \ - Please check whether it is the correct the image meta key." - ) + raise ValueError(f"'affine' is not found in {meta_key}. \ + Please check whether it is the correct the image meta key.") affine: NdarrayOrTensor = meta_dict["affine"] if self.affine_lps_to_ras: # RAS affine @@ -815,16 +809,12 @@ def __init__( ) -> None: box_keys_tuple = ensure_tuple(box_keys) if len(box_keys_tuple) != 1: - raise ValueError( - "Please provide a single key for box_keys.\ - All label_keys are attached to this box_keys." - ) + raise ValueError("Please provide a single key for box_keys.\ + All label_keys are attached to this box_keys.") box_ref_image_keys_tuple = ensure_tuple(box_ref_image_keys) if len(box_ref_image_keys_tuple) != 1: - raise ValueError( - "Please provide a single key for box_ref_image_keys.\ - All box_keys and label_keys are attached to this box_ref_image_keys." - ) + raise ValueError("Please provide a single key for box_ref_image_keys.\ + All box_keys and label_keys are attached to this box_ref_image_keys.") self.label_keys = ensure_tuple(label_keys) super().__init__(box_keys_tuple, allow_missing_keys) @@ -1091,10 +1081,8 @@ def __init__( box_keys_tuple = ensure_tuple(box_keys) if len(box_keys_tuple) != 1: - raise ValueError( - "Please provide a single key for box_keys.\ - All label_keys are attached to this box_keys." - ) + raise ValueError("Please provide a single key for box_keys.\ + All label_keys are attached to this box_keys.") self.box_keys = box_keys_tuple[0] self.label_keys = ensure_tuple(label_keys) diff --git a/monai/apps/detection/utils/anchor_utils.py b/monai/apps/detection/utils/anchor_utils.py index 20f6fc6025..0306a95c7e 100644 --- a/monai/apps/detection/utils/anchor_utils.py +++ b/monai/apps/detection/utils/anchor_utils.py @@ -124,10 +124,8 @@ def __init__( aspect_ratios = (aspect_ratios,) * len(self.sizes) if len(self.sizes) != len(aspect_ratios): - raise ValueError( - "len(sizes) and len(aspect_ratios) should be equal. \ - It represents the number of feature maps." - ) + raise ValueError("len(sizes) and len(aspect_ratios) should be equal. \ + It represents the number of feature maps.") spatial_dims = len(ensure_tuple(aspect_ratios[0][0])) + 1 spatial_dims = look_up_option(spatial_dims, [2, 3]) @@ -172,16 +170,12 @@ def generate_anchors( scales_t = torch.as_tensor(scales, dtype=dtype, device=device) # sized (N,) aspect_ratios_t = torch.as_tensor(aspect_ratios, dtype=dtype, device=device) # sized (M,) or (M,2) if (self.spatial_dims >= 3) and (len(aspect_ratios_t.shape) != 2): - raise ValueError( - f"In {self.spatial_dims}-D image, aspect_ratios for each level should be \ - {len(aspect_ratios_t.shape) - 1}-D. But got aspect_ratios with shape {aspect_ratios_t.shape}." - ) + raise ValueError(f"In {self.spatial_dims}-D image, aspect_ratios for each level should be \ + {len(aspect_ratios_t.shape) - 1}-D. But got aspect_ratios with shape {aspect_ratios_t.shape}.") if (self.spatial_dims >= 3) and (aspect_ratios_t.shape[1] != self.spatial_dims - 1): - raise ValueError( - f"In {self.spatial_dims}-D image, aspect_ratios for each level should has \ - shape (_,{self.spatial_dims - 1}). But got aspect_ratios with shape {aspect_ratios_t.shape}." - ) + raise ValueError(f"In {self.spatial_dims}-D image, aspect_ratios for each level should has \ + shape (_,{self.spatial_dims - 1}). But got aspect_ratios with shape {aspect_ratios_t.shape}.") # if 2d, w:h = 1:aspect_ratios if self.spatial_dims == 2: diff --git a/monai/apps/reconstruction/transforms/array.py b/monai/apps/reconstruction/transforms/array.py index 911d7a06bb..c1a43043e4 100644 --- a/monai/apps/reconstruction/transforms/array.py +++ b/monai/apps/reconstruction/transforms/array.py @@ -61,10 +61,8 @@ def __init__( real/imaginary parts. """ if len(center_fractions) != len(accelerations): - raise ValueError( - "Number of center fractions \ - should match number of accelerations" - ) + raise ValueError("Number of center fractions \ + should match number of accelerations") self.center_fractions = center_fractions self.accelerations = accelerations diff --git a/monai/auto3dseg/analyzer.py b/monai/auto3dseg/analyzer.py index 8f84e2ebcc..6a77e08974 100644 --- a/monai/auto3dseg/analyzer.py +++ b/monai/auto3dseg/analyzer.py @@ -105,7 +105,7 @@ def update_ops_nested_label(self, nested_key: str, op: Operations) -> None: raise ValueError("Nested_key input format is wrong. Please ensure it is like key1#0#key2") root: str child_key: str - (root, _, child_key) = keys + root, _, child_key = keys if root not in self.ops: self.ops[root] = [{}] self.ops[root][0].update({child_key: None}) @@ -471,8 +471,7 @@ def __call__(self, data: Mapping[Hashable, MetaTensor]) -> dict[Hashable, MetaTe image_tensor = d[self.image_key] label_tensor = d[self.label_key] using_cuda = any( - isinstance(t, (torch.Tensor, MetaTensor)) and t.device.type == "cuda" - for t in (image_tensor, label_tensor) + isinstance(t, (torch.Tensor, MetaTensor)) and t.device.type == "cuda" for t in (image_tensor, label_tensor) ) restore_grad_state = torch.is_grad_enabled() torch.set_grad_enabled(False) diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index 9fdee6acd0..fa9ba27096 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -1948,7 +1948,7 @@ def create_workflow( """ _args = update_kwargs(args=args_file, workflow_name=workflow_name, config_file=config_file, **kwargs) - (workflow_name, config_file) = _pop_args( + workflow_name, config_file = _pop_args( _args, workflow_name=ConfigWorkflow, config_file=None ) # the default workflow name is "ConfigWorkflow" if isinstance(workflow_name, str): diff --git a/monai/bundle/utils.py b/monai/bundle/utils.py index 53d619f234..d37d7f1c05 100644 --- a/monai/bundle/utils.py +++ b/monai/bundle/utils.py @@ -124,10 +124,8 @@ "run_name": None, # may fill it at runtime "save_execute_config": True, - "is_not_rank0": ( - "$torch.distributed.is_available() \ - and torch.distributed.is_initialized() and torch.distributed.get_rank() > 0" - ), + "is_not_rank0": ("$torch.distributed.is_available() \ + and torch.distributed.is_initialized() and torch.distributed.get_rank() > 0"), # MLFlowHandler config for the trainer "trainer": { "_target_": "MLFlowHandler", diff --git a/monai/data/dataset.py b/monai/data/dataset.py index 066cec41b7..21b24840b5 100644 --- a/monai/data/dataset.py +++ b/monai/data/dataset.py @@ -139,7 +139,7 @@ class DatasetFunc(Dataset): """ def __init__(self, data: Any, func: Callable, **kwargs) -> None: - super().__init__(data=None, transform=None) # type:ignore + super().__init__(data=None, transform=None) # type: ignore self.src = data self.func = func self.kwargs = kwargs @@ -1635,7 +1635,7 @@ def _cachecheck(self, item_transformed): return (_data, _meta) return _data else: - item: list[dict[Any, Any]] = [{} for _ in range(len(item_transformed))] # type:ignore + item: list[dict[Any, Any]] = [{} for _ in range(len(item_transformed))] # type: ignore for i, _item in enumerate(item_transformed): for k in _item: meta_i_k = self._load_meta_cache(meta_hash_file_name=f"{hashfile.name}-{k}-meta-{i}") diff --git a/monai/handlers/utils.py b/monai/handlers/utils.py index b6771f2dcc..02975039b3 100644 --- a/monai/handlers/utils.py +++ b/monai/handlers/utils.py @@ -48,7 +48,7 @@ def stopping_fn_from_loss() -> Callable[[Engine], Any]: """ def stopping_fn(engine: Engine) -> Any: - return -engine.state.output # type:ignore + return -engine.state.output # type: ignore return stopping_fn diff --git a/monai/losses/dice.py b/monai/losses/dice.py index 948749606b..cec9969c12 100644 --- a/monai/losses/dice.py +++ b/monai/losses/dice.py @@ -204,11 +204,9 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor: self.class_weight = torch.as_tensor([self.class_weight] * num_of_classes) else: if self.class_weight.shape[0] != num_of_classes: - raise ValueError( - """the length of the `weight` sequence should be the same as the number of classes. + raise ValueError("""the length of the `weight` sequence should be the same as the number of classes. If `include_background=False`, the weight should not include - the background category class 0.""" - ) + the background category class 0.""") if self.class_weight.min() < 0: raise ValueError("the value/values of the `weight` should be no less than 0.") # apply class_weight to loss diff --git a/monai/losses/focal_loss.py b/monai/losses/focal_loss.py index caa237fca8..7ab54c319d 100644 --- a/monai/losses/focal_loss.py +++ b/monai/losses/focal_loss.py @@ -183,11 +183,9 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor: self.class_weight = torch.as_tensor([self.class_weight] * num_of_classes) else: if self.class_weight.shape[0] != num_of_classes: - raise ValueError( - """the length of the `weight` sequence should be the same as the number of classes. + raise ValueError("""the length of the `weight` sequence should be the same as the number of classes. If `include_background=False`, the weight should not include - the background category class 0.""" - ) + the background category class 0.""") if self.class_weight.min() < 0: raise ValueError("the value/values of the `weight` should be no less than 0.") # apply class_weight to loss diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py index a451b1a770..4a60e438cf 100644 --- a/monai/metrics/utils.py +++ b/monai/metrics/utils.py @@ -320,7 +320,7 @@ def get_edge_surface_distance( edges_spacing = None if use_subvoxels: edges_spacing = spacing if spacing is not None else ([1] * len(y_pred.shape)) - (edges_pred, edges_gt, *areas) = get_mask_edges( + edges_pred, edges_gt, *areas = get_mask_edges( y_pred, y, crop=True, spacing=edges_spacing, always_return_as_numpy=False ) if not edges_gt.any(): diff --git a/monai/transforms/io/array.py b/monai/transforms/io/array.py index 0628a7fbc4..f0c1d1949d 100644 --- a/monai/transforms/io/array.py +++ b/monai/transforms/io/array.py @@ -11,6 +11,7 @@ """ A collection of "vanilla" transforms for IO functions. """ + from __future__ import annotations import inspect diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 3dc7897feb..7df6e2c5ef 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -702,7 +702,7 @@ def __init__( # if the root log level is higher than INFO, set a separate stream handler to record console = logging.StreamHandler(sys.stdout) console.setLevel(logging.INFO) - console.is_data_stats_handler = True # type:ignore[attr-defined] + console.is_data_stats_handler = True # type: ignore[attr-defined] _logger.addHandler(console) def __call__( diff --git a/tests/integration/test_loader_semaphore.py b/tests/integration/test_loader_semaphore.py index 78baedc264..c32bcb0b8b 100644 --- a/tests/integration/test_loader_semaphore.py +++ b/tests/integration/test_loader_semaphore.py @@ -10,6 +10,7 @@ # limitations under the License. """this test should not generate errors or UserWarning: semaphore_tracker: There appear to be 1 leaked semaphores""" + from __future__ import annotations import multiprocessing as mp diff --git a/tests/profile_subclass/profiling.py b/tests/profile_subclass/profiling.py index 18aecea2fb..6106259526 100644 --- a/tests/profile_subclass/profiling.py +++ b/tests/profile_subclass/profiling.py @@ -12,6 +12,7 @@ Comparing torch.Tensor, SubTensor, SubWithTorchFunc, MetaTensor Adapted from https://github.com/pytorch/pytorch/tree/v1.11.0/benchmarks/overrides_benchmark """ + from __future__ import annotations import argparse diff --git a/tests/profile_subclass/pyspy_profiling.py b/tests/profile_subclass/pyspy_profiling.py index fac425f577..671dc74c01 100644 --- a/tests/profile_subclass/pyspy_profiling.py +++ b/tests/profile_subclass/pyspy_profiling.py @@ -12,6 +12,7 @@ To be used with py-spy, comparing torch.Tensor, SubTensor, SubWithTorchFunc, MetaTensor Adapted from https://github.com/pytorch/pytorch/tree/v1.11.0/benchmarks/overrides_benchmark """ + from __future__ import annotations import argparse diff --git a/versioneer.py b/versioneer.py index a06587fc3f..6839363323 100644 --- a/versioneer.py +++ b/versioneer.py @@ -273,6 +273,7 @@ [travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer """ + # pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring # pylint:disable=missing-class-docstring,too-many-branches,too-many-statements # pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error @@ -428,9 +429,7 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= return stdout, process.returncode -LONG_VERSION_PY[ - "git" -] = r''' +LONG_VERSION_PY["git"] = r''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build From 1a2598b5ab0624fce4e9ab74cd4eed9f922fb801 Mon Sep 17 00:00:00 2001 From: Rafael Garcia-Dias Date: Tue, 17 Mar 2026 17:14:03 +0000 Subject: [PATCH 7/7] Add mixed-device LabelStats handling and tests (#1) * Add mixed-device LabelStats coverage * Autofix to mirror CI: Restrict versions of isort, ruff, and black. Signed-off-by: R. Garcia-Dias --------- Signed-off-by: R. Garcia-Dias --- monai/apps/detection/transforms/dictionary.py | 36 ++++++++----- monai/apps/detection/utils/anchor_utils.py | 18 ++++--- monai/apps/reconstruction/transforms/array.py | 6 ++- monai/auto3dseg/analyzer.py | 6 +++ monai/bundle/utils.py | 6 ++- monai/losses/dice.py | 6 ++- monai/losses/focal_loss.py | 6 ++- requirements-dev.txt | 6 +-- tests/apps/test_auto3dseg.py | 53 ++++++++++++++++++- versioneer.py | 4 +- 10 files changed, 116 insertions(+), 31 deletions(-) diff --git a/monai/apps/detection/transforms/dictionary.py b/monai/apps/detection/transforms/dictionary.py index f7c1ce0770..f52ab53531 100644 --- a/monai/apps/detection/transforms/dictionary.py +++ b/monai/apps/detection/transforms/dictionary.py @@ -125,8 +125,10 @@ def __init__(self, box_keys: KeysCollection, box_ref_image_keys: str, allow_miss super().__init__(box_keys, allow_missing_keys) box_ref_image_keys_tuple = ensure_tuple(box_ref_image_keys) if len(box_ref_image_keys_tuple) > 1: - raise ValueError("Please provide a single key for box_ref_image_keys.\ - All boxes of box_keys are attached to box_ref_image_keys.") + raise ValueError( + "Please provide a single key for box_ref_image_keys.\ + All boxes of box_keys are attached to box_ref_image_keys." + ) self.box_ref_image_keys = box_ref_image_keys def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, NdarrayOrTensor]: @@ -287,8 +289,10 @@ def __init__( super().__init__(box_keys, allow_missing_keys) box_ref_image_keys_tuple = ensure_tuple(box_ref_image_keys) if len(box_ref_image_keys_tuple) > 1: - raise ValueError("Please provide a single key for box_ref_image_keys.\ - All boxes of box_keys are attached to box_ref_image_keys.") + raise ValueError( + "Please provide a single key for box_ref_image_keys.\ + All boxes of box_keys are attached to box_ref_image_keys." + ) self.box_ref_image_keys = box_ref_image_keys self.image_meta_key = image_meta_key or f"{box_ref_image_keys}_{image_meta_key_postfix}" self.converter_to_image_coordinate = AffineBox() @@ -306,8 +310,10 @@ def extract_affine(self, data: Mapping[Hashable, torch.Tensor]) -> tuple[Ndarray else: raise ValueError(f"{meta_key} is not found. Please check whether it is the correct the image meta key.") if "affine" not in meta_dict: - raise ValueError(f"'affine' is not found in {meta_key}. \ - Please check whether it is the correct the image meta key.") + raise ValueError( + f"'affine' is not found in {meta_key}. \ + Please check whether it is the correct the image meta key." + ) affine: NdarrayOrTensor = meta_dict["affine"] if self.affine_lps_to_ras: # RAS affine @@ -809,12 +815,16 @@ def __init__( ) -> None: box_keys_tuple = ensure_tuple(box_keys) if len(box_keys_tuple) != 1: - raise ValueError("Please provide a single key for box_keys.\ - All label_keys are attached to this box_keys.") + raise ValueError( + "Please provide a single key for box_keys.\ + All label_keys are attached to this box_keys." + ) box_ref_image_keys_tuple = ensure_tuple(box_ref_image_keys) if len(box_ref_image_keys_tuple) != 1: - raise ValueError("Please provide a single key for box_ref_image_keys.\ - All box_keys and label_keys are attached to this box_ref_image_keys.") + raise ValueError( + "Please provide a single key for box_ref_image_keys.\ + All box_keys and label_keys are attached to this box_ref_image_keys." + ) self.label_keys = ensure_tuple(label_keys) super().__init__(box_keys_tuple, allow_missing_keys) @@ -1081,8 +1091,10 @@ def __init__( box_keys_tuple = ensure_tuple(box_keys) if len(box_keys_tuple) != 1: - raise ValueError("Please provide a single key for box_keys.\ - All label_keys are attached to this box_keys.") + raise ValueError( + "Please provide a single key for box_keys.\ + All label_keys are attached to this box_keys." + ) self.box_keys = box_keys_tuple[0] self.label_keys = ensure_tuple(label_keys) diff --git a/monai/apps/detection/utils/anchor_utils.py b/monai/apps/detection/utils/anchor_utils.py index 0306a95c7e..20f6fc6025 100644 --- a/monai/apps/detection/utils/anchor_utils.py +++ b/monai/apps/detection/utils/anchor_utils.py @@ -124,8 +124,10 @@ def __init__( aspect_ratios = (aspect_ratios,) * len(self.sizes) if len(self.sizes) != len(aspect_ratios): - raise ValueError("len(sizes) and len(aspect_ratios) should be equal. \ - It represents the number of feature maps.") + raise ValueError( + "len(sizes) and len(aspect_ratios) should be equal. \ + It represents the number of feature maps." + ) spatial_dims = len(ensure_tuple(aspect_ratios[0][0])) + 1 spatial_dims = look_up_option(spatial_dims, [2, 3]) @@ -170,12 +172,16 @@ def generate_anchors( scales_t = torch.as_tensor(scales, dtype=dtype, device=device) # sized (N,) aspect_ratios_t = torch.as_tensor(aspect_ratios, dtype=dtype, device=device) # sized (M,) or (M,2) if (self.spatial_dims >= 3) and (len(aspect_ratios_t.shape) != 2): - raise ValueError(f"In {self.spatial_dims}-D image, aspect_ratios for each level should be \ - {len(aspect_ratios_t.shape) - 1}-D. But got aspect_ratios with shape {aspect_ratios_t.shape}.") + raise ValueError( + f"In {self.spatial_dims}-D image, aspect_ratios for each level should be \ + {len(aspect_ratios_t.shape) - 1}-D. But got aspect_ratios with shape {aspect_ratios_t.shape}." + ) if (self.spatial_dims >= 3) and (aspect_ratios_t.shape[1] != self.spatial_dims - 1): - raise ValueError(f"In {self.spatial_dims}-D image, aspect_ratios for each level should has \ - shape (_,{self.spatial_dims - 1}). But got aspect_ratios with shape {aspect_ratios_t.shape}.") + raise ValueError( + f"In {self.spatial_dims}-D image, aspect_ratios for each level should has \ + shape (_,{self.spatial_dims - 1}). But got aspect_ratios with shape {aspect_ratios_t.shape}." + ) # if 2d, w:h = 1:aspect_ratios if self.spatial_dims == 2: diff --git a/monai/apps/reconstruction/transforms/array.py b/monai/apps/reconstruction/transforms/array.py index c1a43043e4..911d7a06bb 100644 --- a/monai/apps/reconstruction/transforms/array.py +++ b/monai/apps/reconstruction/transforms/array.py @@ -61,8 +61,10 @@ def __init__( real/imaginary parts. """ if len(center_fractions) != len(accelerations): - raise ValueError("Number of center fractions \ - should match number of accelerations") + raise ValueError( + "Number of center fractions \ + should match number of accelerations" + ) self.center_fractions = center_fractions self.accelerations = accelerations diff --git a/monai/auto3dseg/analyzer.py b/monai/auto3dseg/analyzer.py index 6a77e08974..c313effbea 100644 --- a/monai/auto3dseg/analyzer.py +++ b/monai/auto3dseg/analyzer.py @@ -476,6 +476,12 @@ def __call__(self, data: Mapping[Hashable, MetaTensor]) -> dict[Hashable, MetaTe restore_grad_state = torch.is_grad_enabled() torch.set_grad_enabled(False) + if isinstance(image_tensor, (MetaTensor, torch.Tensor)) and isinstance( + label_tensor, (MetaTensor, torch.Tensor) + ): + if label_tensor.device != image_tensor.device: + label_tensor = label_tensor.to(image_tensor.device) + ndas: list[MetaTensor] = [image_tensor[i] for i in range(image_tensor.shape[0])] # type: ignore ndas_label: MetaTensor = label_tensor.astype(torch.int16) # (H,W,D) diff --git a/monai/bundle/utils.py b/monai/bundle/utils.py index d37d7f1c05..53d619f234 100644 --- a/monai/bundle/utils.py +++ b/monai/bundle/utils.py @@ -124,8 +124,10 @@ "run_name": None, # may fill it at runtime "save_execute_config": True, - "is_not_rank0": ("$torch.distributed.is_available() \ - and torch.distributed.is_initialized() and torch.distributed.get_rank() > 0"), + "is_not_rank0": ( + "$torch.distributed.is_available() \ + and torch.distributed.is_initialized() and torch.distributed.get_rank() > 0" + ), # MLFlowHandler config for the trainer "trainer": { "_target_": "MLFlowHandler", diff --git a/monai/losses/dice.py b/monai/losses/dice.py index d757db2557..cd76ec1323 100644 --- a/monai/losses/dice.py +++ b/monai/losses/dice.py @@ -203,9 +203,11 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor: self.class_weight = torch.as_tensor([self.class_weight] * num_of_classes) else: if self.class_weight.shape[0] != num_of_classes: - raise ValueError("""the length of the `weight` sequence should be the same as the number of classes. + raise ValueError( + """the length of the `weight` sequence should be the same as the number of classes. If `include_background=False`, the weight should not include - the background category class 0.""") + the background category class 0.""" + ) if self.class_weight.min() < 0: raise ValueError("the value/values of the `weight` should be no less than 0.") # apply class_weight to loss diff --git a/monai/losses/focal_loss.py b/monai/losses/focal_loss.py index 7ab54c319d..caa237fca8 100644 --- a/monai/losses/focal_loss.py +++ b/monai/losses/focal_loss.py @@ -183,9 +183,11 @@ def forward(self, input: torch.Tensor, target: torch.Tensor) -> torch.Tensor: self.class_weight = torch.as_tensor([self.class_weight] * num_of_classes) else: if self.class_weight.shape[0] != num_of_classes: - raise ValueError("""the length of the `weight` sequence should be the same as the number of classes. + raise ValueError( + """the length of the `weight` sequence should be the same as the number of classes. If `include_background=False`, the weight should not include - the background category class 0.""") + the background category class 0.""" + ) if self.class_weight.min() < 0: raise ValueError("the value/values of the `weight` should be no less than 0.") # apply class_weight to loss diff --git a/requirements-dev.txt b/requirements-dev.txt index 3e9189a1c3..16d91a39a7 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,9 +14,9 @@ mccabe pep8-naming pycodestyle pyflakes -black>=25.1.0 -isort>=5.1, !=6.0.0 -ruff +black==25.1.0 +isort>=5.1, <6, !=6.0.0 +ruff>=0.14.11,<0.15 pytype>=2020.6.1, <=2024.4.11; platform_system != "Windows" types-setuptools mypy>=1.5.0, <1.12.0 diff --git a/tests/apps/test_auto3dseg.py b/tests/apps/test_auto3dseg.py index 6c0d8123d7..2159265873 100644 --- a/tests/apps/test_auto3dseg.py +++ b/tests/apps/test_auto3dseg.py @@ -53,7 +53,7 @@ SqueezeDimd, ToDeviced, ) -from monai.utils.enums import DataStatsKeys +from monai.utils.enums import DataStatsKeys, LabelStatsKeys from tests.test_utils import skip_if_no_cuda device = "cpu" @@ -78,6 +78,13 @@ SIM_GPU_TEST_CASES = [[{"sim_dim": (32, 32, 32), "label_key": "label"}], [{"sim_dim": (32, 32, 32), "label_key": None}]] +LABEL_STATS_DEVICE_TEST_CASES = [ + [{"image_device": "cpu", "label_device": "cpu", "image_meta": False}], + [{"image_device": "cuda", "label_device": "cuda", "image_meta": True}], + [{"image_device": "cpu", "label_device": "cuda", "image_meta": True}], + [{"image_device": "cuda", "label_device": "cpu", "image_meta": False}], +] + def create_sim_data(dataroot: str, sim_datalist: dict, sim_dim: tuple, image_only: bool = False, **kwargs) -> None: """ @@ -360,6 +367,50 @@ def test_label_stats_case_analyzer(self): report_format = analyzer.get_report_format() assert verify_report_format(d["label_stats"], report_format) + @parameterized.expand(LABEL_STATS_DEVICE_TEST_CASES) + def test_label_stats_mixed_device_analyzer(self, input_params): + image_device = torch.device(input_params["image_device"]) + label_device = torch.device(input_params["label_device"]) + + if (image_device.type == "cuda" or label_device.type == "cuda") and not torch.cuda.is_available(): + self.skipTest("CUDA is not available for mixed-device LabelStats tests.") + + analyzer = LabelStats(image_key="image", label_key="label") + + image_tensor = torch.tensor( + [ + [[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]]], + [[[11.0, 12.0], [13.0, 14.0]], [[15.0, 16.0], [17.0, 18.0]]], + ], + dtype=torch.float32, + ).to(image_device) + label_tensor = torch.tensor([[[0, 1], [1, 0]], [[0, 1], [0, 1]]], dtype=torch.int64).to(label_device) + + if input_params["image_meta"]: + image_tensor = MetaTensor(image_tensor) + label_tensor = MetaTensor(label_tensor) + + result = analyzer({"image": image_tensor, "label": label_tensor}) + report = result["label_stats"] + + assert verify_report_format(report, analyzer.get_report_format()) + assert report[LabelStatsKeys.LABEL_UID] == [0, 1] + + label_stats = report[LabelStatsKeys.LABEL] + self.assertAlmostEqual(label_stats[0][LabelStatsKeys.PIXEL_PCT], 0.5) + self.assertAlmostEqual(label_stats[1][LabelStatsKeys.PIXEL_PCT], 0.5) + + label0_intensity = label_stats[0][LabelStatsKeys.IMAGE_INTST] + label1_intensity = label_stats[1][LabelStatsKeys.IMAGE_INTST] + self.assertAlmostEqual(label0_intensity[0]["mean"], 4.25) + self.assertAlmostEqual(label1_intensity[0]["mean"], 4.75) + self.assertAlmostEqual(label0_intensity[1]["mean"], 14.25) + self.assertAlmostEqual(label1_intensity[1]["mean"], 14.75) + + foreground_stats = report[LabelStatsKeys.IMAGE_INTST] + self.assertAlmostEqual(foreground_stats[0]["mean"], 4.75) + self.assertAlmostEqual(foreground_stats[1]["mean"], 14.75) + def test_filename_case_analyzer(self): analyzer_image = FilenameStats("image", DataStatsKeys.BY_CASE_IMAGE_PATH) analyzer_label = FilenameStats("label", DataStatsKeys.BY_CASE_IMAGE_PATH) diff --git a/versioneer.py b/versioneer.py index 6839363323..5d0a606c91 100644 --- a/versioneer.py +++ b/versioneer.py @@ -429,7 +429,9 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= return stdout, process.returncode -LONG_VERSION_PY["git"] = r''' +LONG_VERSION_PY[ + "git" +] = r''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build