From 4b04d58a0de8a8f81ceaefe6e8e093c43cbf0e26 Mon Sep 17 00:00:00 2001 From: Vikas Reddy Date: Wed, 14 Oct 2026 19:33:54 +0000 Subject: [PATCH 1/6] Fix eager nvimgcodec plugin import Signed-off-by: Vikas Reddy --- monai/__init__.py | 1 + tests/data/test_init_reader.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/monai/__init__.py b/monai/__init__.py index 2d6f5dfc37..db13712b35 100644 --- a/monai/__init__.py +++ b/monai/__init__.py @@ -86,6 +86,7 @@ def filter(self, record): # *.so files and folder "_C" may not exist when the cpp extensions are not compiled excludes = "|".join( [ + "(.*(nvimgcodec_pydicom_plugin)$)", "(^(monai.handlers))", "(^(monai.bundle))", "(^(monai.fl))", diff --git a/tests/data/test_init_reader.py b/tests/data/test_init_reader.py index 4cd9ac42c4..d35d77e977 100644 --- a/tests/data/test_init_reader.py +++ b/tests/data/test_init_reader.py @@ -12,6 +12,8 @@ from __future__ import annotations import os +import subprocess +import sys import tempfile import unittest @@ -65,6 +67,17 @@ def test_load_image(self): inst = LoadImaged("image", reader="NvImgCodecPydicomReader") self.assertIsInstance(inst, LoadImaged) + def test_nvimgcodec_not_imported_on_monai_import(self): + _, has_nvimgcodec = optional_import("nvidia.nvimgcodec") + if not has_nvimgcodec: + self.skipTest("nvidia.nvimgcodec is not installed") + + code = "import monai, sys; print('nvidia.nvimgcodec' in sys.modules)" + result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), "False") + @SkipIfNoModule("nibabel") @SkipIfNoModule("cupy") @SkipIfNoModule("kvikio") From 4500999c3d3538d4cedaac23f2db871f58356b41 Mon Sep 17 00:00:00 2001 From: Vikas Reddy Date: Thu, 15 Oct 2026 09:27:59 +0000 Subject: [PATCH 2/6] Add docstring to nvimgcodec import test Signed-off-by: Vikas Reddy --- tests/data/test_init_reader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/data/test_init_reader.py b/tests/data/test_init_reader.py index d35d77e977..3b13aa4edc 100644 --- a/tests/data/test_init_reader.py +++ b/tests/data/test_init_reader.py @@ -68,6 +68,7 @@ def test_load_image(self): self.assertIsInstance(inst, LoadImaged) def test_nvimgcodec_not_imported_on_monai_import(self): + """Verify importing MONAI does not eagerly load the optional NVIDIA package.""" _, has_nvimgcodec = optional_import("nvidia.nvimgcodec") if not has_nvimgcodec: self.skipTest("nvidia.nvimgcodec is not installed") From 253820f449701926fabf739155b7db2d83a12b46 Mon Sep 17 00:00:00 2001 From: Vikas Reddy Date: Thu, 15 Oct 2026 09:42:32 +0000 Subject: [PATCH 3/6] Check nvimgcodec availability without importing it Signed-off-by: Vikas Reddy --- tests/data/test_init_reader.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/data/test_init_reader.py b/tests/data/test_init_reader.py index 3b13aa4edc..d2ab670791 100644 --- a/tests/data/test_init_reader.py +++ b/tests/data/test_init_reader.py @@ -16,6 +16,7 @@ import sys import tempfile import unittest +from importlib.machinery import PathFinder import numpy as np @@ -69,8 +70,12 @@ def test_load_image(self): def test_nvimgcodec_not_imported_on_monai_import(self): """Verify importing MONAI does not eagerly load the optional NVIDIA package.""" - _, has_nvimgcodec = optional_import("nvidia.nvimgcodec") - if not has_nvimgcodec: + nvidia_spec = PathFinder.find_spec("nvidia") + if nvidia_spec is None or nvidia_spec.submodule_search_locations is None: + self.skipTest("nvidia.nvimgcodec is not installed") + + nvimgcodec_spec = PathFinder.find_spec("nvimgcodec", nvidia_spec.submodule_search_locations) + if nvimgcodec_spec is None: self.skipTest("nvidia.nvimgcodec is not installed") code = "import monai, sys; print('nvidia.nvimgcodec' in sys.modules)" From d3f752c5193f316224316c853fa0656af6857631 Mon Sep 17 00:00:00 2001 From: Vikas Reddy Date: Thu, 15 Oct 2026 09:53:41 +0000 Subject: [PATCH 4/6] Assert no stderr from nvimgcodec import test Signed-off-by: Vikas Reddy --- tests/data/test_init_reader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/data/test_init_reader.py b/tests/data/test_init_reader.py index d2ab670791..240816d7df 100644 --- a/tests/data/test_init_reader.py +++ b/tests/data/test_init_reader.py @@ -83,6 +83,7 @@ def test_nvimgcodec_not_imported_on_monai_import(self): self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.stdout.strip(), "False") + self.assertEqual(result.stderr, "") @SkipIfNoModule("nibabel") @SkipIfNoModule("cupy") From e403911482c53178b44628b4016a9e659b03cc01 Mon Sep 17 00:00:00 2001 From: Vikas Reddy Date: Sun, 18 Oct 2026 08:08:40 +0000 Subject: [PATCH 5/6] Make nvimgcodec reader setup lazy Signed-off-by: Vikas Reddy --- monai/data/image_reader.py | 15 +++++++++++++ monai/data/nvimgcodec_pydicom_plugin.py | 2 +- tests/data/test_init_reader.py | 22 ++++++++++++++++++++ tests/data/test_nvimgcodec_pydicom_reader.py | 7 ++++++- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 0a853372f4..f6ed1858f4 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -1159,6 +1159,14 @@ def __init__( to_gpu=False, **kwargs, ) + + self._nvimgcodec_available = False + self._setup_done = False + + def _ensure_setup(self) -> None: + """Import and register the nvImageCodec plugin on first use only.""" + if self._setup_done: + return from monai.data.nvimgcodec_pydicom_plugin import is_nvimgcodec_available, register_as_decoder_plugin self._nvimgcodec_available = is_nvimgcodec_available() @@ -1167,15 +1175,22 @@ def __init__( "NvImgCodecPydicomReader: nvImageCodec decoder plugin did not register successfully. " "Falling back to default pydicom decoders." ) + self._setup_done = True def verify_suffix(self, filename: Sequence[PathLike] | PathLike) -> bool: """ Verify whether the specified file or files are DICOM and nvImageCodec is available. """ + self._ensure_setup() if not has_pydicom or not self._nvimgcodec_available: return False return is_dicom_path(filename) + def read(self, data: Sequence[PathLike] | PathLike, **kwargs): + """Set up the nvImageCodec plugin on first use, then read as PydicomReader does.""" + self._ensure_setup() + return super().read(data, **kwargs) + @require_pkg(pkg_name="nibabel") class NibabelReader(ImageReader): diff --git a/monai/data/nvimgcodec_pydicom_plugin.py b/monai/data/nvimgcodec_pydicom_plugin.py index 573dc715d4..07ba5ca726 100644 --- a/monai/data/nvimgcodec_pydicom_plugin.py +++ b/monai/data/nvimgcodec_pydicom_plugin.py @@ -66,7 +66,7 @@ def is_nvimgcodec_available() -> bool: def register_as_decoder_plugin(module_path: str | None = None) -> bool: """Register the nvImageCodec pydicom decoder plugin.""" if not is_nvimgcodec_available(): - _logger.warning("nvImageCodec is not available; skipping pydicom decoder plugin registration.") + _logger.debug("nvImageCodec is not available; skipping pydicom decoder plugin registration.") return False if not has_pydicom_plugin: return False diff --git a/tests/data/test_init_reader.py b/tests/data/test_init_reader.py index 240816d7df..39347c9c47 100644 --- a/tests/data/test_init_reader.py +++ b/tests/data/test_init_reader.py @@ -85,6 +85,28 @@ def test_nvimgcodec_not_imported_on_monai_import(self): self.assertEqual(result.stdout.strip(), "False") self.assertEqual(result.stderr, "") + def test_nvimgcodec_not_imported_on_loadimage_init(self): + """Verify initializing LoadImage does not eagerly load nvimgcodec.""" + nvidia_spec = PathFinder.find_spec("nvidia") + if nvidia_spec is None or nvidia_spec.submodule_search_locations is None: + self.skipTest("nvidia.nvimgcodec is not installed") + + nvimgcodec_spec = PathFinder.find_spec("nvimgcodec", nvidia_spec.submodule_search_locations) + if nvimgcodec_spec is None: + self.skipTest("nvidia.nvimgcodec is not installed") + + code = ( + "import monai, sys; " + "from monai.transforms import LoadImage; " + "LoadImage(); " + "print('nvidia.nvimgcodec' in sys.modules)" + ) + result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout.strip(), "False") + self.assertEqual(result.stderr, "") + @SkipIfNoModule("nibabel") @SkipIfNoModule("cupy") @SkipIfNoModule("kvikio") diff --git a/tests/data/test_nvimgcodec_pydicom_reader.py b/tests/data/test_nvimgcodec_pydicom_reader.py index 46520dc320..8e6e41f919 100644 --- a/tests/data/test_nvimgcodec_pydicom_reader.py +++ b/tests/data/test_nvimgcodec_pydicom_reader.py @@ -100,11 +100,16 @@ class TestNvImgCodecPydicomReader(unittest.TestCase): @SkipIfNoModule("pydicom") @patch("monai.data.nvimgcodec_pydicom_plugin.register_as_decoder_plugin", return_value=True) @patch("monai.data.nvimgcodec_pydicom_plugin.is_nvimgcodec_available", return_value=True) - def test_reader_init_registers_plugin(self, _mock_available, mock_register): + def test_reader_registers_plugin_lazily(self, _mock_available, mock_register): from monai.data import NvImgCodecPydicomReader reader = NvImgCodecPydicomReader() self.assertIsInstance(reader, NvImgCodecPydicomReader) + + mock_register.assert_not_called() + + reader.verify_suffix("x.dcm") + mock_register.assert_called_once() @SkipIfNoModule("pydicom") From d796e385a1d59952f5cd38aeb4cbdcb6d65956e4 Mon Sep 17 00:00:00 2001 From: Vikas Reddy Date: Mon, 19 Oct 2026 05:40:03 +0000 Subject: [PATCH 6/6] Make nvimgcodec reader setup lazy Signed-off-by: Vikas Reddy --- monai/data/image_reader.py | 21 +++++++++++++------- tests/data/test_nvimgcodec_pydicom_reader.py | 10 ++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index f6ed1858f4..528a986228 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -1087,7 +1087,7 @@ class NvImgCodecPydicomReader(PydicomReader): Load DICOM images using Pydicom with GPU-accelerated decompression via nvImageCodec. This reader extends :py:class:`PydicomReader` and registers the nvImageCodec pydicom - decoder plugin on initialization. The plugin accelerates decoding of compressed pixel data + decoder plugin lazily on first use. The plugin accelerates decoding of compressed pixel data for JPEG, JPEG 2000, and HTJ2K transfer syntaxes when CUDA, CuPy and ``nvidia-nvimgcodec`` are available. If nvImageCodec is not available, a warning is issued and the reader falls back to the @@ -1117,8 +1117,8 @@ class NvImgCodecPydicomReader(PydicomReader): This reader only declares ``@require_pkg(pkg_name="pydicom")`` so that :py:class:`monai.transforms.LoadImage` can register it without hard-failing when GPU dependencies are missing. ``pydicom`` is required to construct - the reader; nvimgcodec, CUDA, and CuPy availability is checked at runtime with a warning issued and fallback to - default pydicom decoders if missing. + the reader; nvImageCodec, CUDA, and CuPy availability is checked when a DICOM file is verified or read. + If the plugin is unavailable or registration fails, the reader falls back to the default pydicom decoders. Args: channel_dim: the channel dimension of the input image, default is None. @@ -1181,13 +1181,20 @@ def verify_suffix(self, filename: Sequence[PathLike] | PathLike) -> bool: """ Verify whether the specified file or files are DICOM and nvImageCodec is available. """ - self._ensure_setup() - if not has_pydicom or not self._nvimgcodec_available: + if not has_pydicom or not is_dicom_path(filename): return False - return is_dicom_path(filename) + self._ensure_setup() + return self._nvimgcodec_available def read(self, data: Sequence[PathLike] | PathLike, **kwargs): - """Set up the nvImageCodec plugin on first use, then read as PydicomReader does.""" + """Read image data using the configured reader. + + Args: + data: Input image data or file path. + **kwargs: Additional arguments passed to the underlying reader. + + Returns: + The image data returned by the underlying reader.""" self._ensure_setup() return super().read(data, **kwargs) diff --git a/tests/data/test_nvimgcodec_pydicom_reader.py b/tests/data/test_nvimgcodec_pydicom_reader.py index 8e6e41f919..f25aa71d82 100644 --- a/tests/data/test_nvimgcodec_pydicom_reader.py +++ b/tests/data/test_nvimgcodec_pydicom_reader.py @@ -112,6 +112,16 @@ def test_reader_registers_plugin_lazily(self, _mock_available, mock_register): mock_register.assert_called_once() + @SkipIfNoModule("pydicom") + @patch("monai.data.nvimgcodec_pydicom_plugin.register_as_decoder_plugin", return_value=True) + @patch("monai.data.nvimgcodec_pydicom_plugin.is_nvimgcodec_available", return_value=True) + def test_reader_does_not_register_plugin_for_non_dicom(self, _mock_available, mock_register): + from monai.data import NvImgCodecPydicomReader + + reader = NvImgCodecPydicomReader() + self.assertFalse(reader.verify_suffix("x.png")) + mock_register.assert_not_called() + @SkipIfNoModule("pydicom") @patch("monai.data.nvimgcodec_pydicom_plugin.is_nvimgcodec_available", return_value=False) def test_verify_suffix_without_nvimgcodec(self, _mock_available):