diff --git a/.dockerignore b/.dockerignore index 19bbf580ba..fc8d7e27d2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,11 +3,15 @@ __pycache__/ docs/ +.vscode +.git +.mypy_cache +.ruff_cache +.pytype .coverage .coverage.* .coverage/ coverage.xml .readthedocs.yml -*.toml !README.md diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index fca9136763..13fe197733 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -28,6 +28,14 @@ jobs: matrix: opt: ["codeformat", "pytype", "mypy"] steps: + - name: Clean unused tools + run: | + find /opt/hostedtoolcache/* -maxdepth 0 ! -name 'Python' -exec rm -rf {} \; + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc /usr/local/.ghcup + sudo docker system prune -f + - uses: actions/checkout@v6 - name: Set up Python 3.9 uses: actions/setup-python@v6 @@ -129,6 +137,14 @@ jobs: QUICKTEST: True shell: bash steps: + - name: Clean unused tools + run: | + find /opt/hostedtoolcache/* -maxdepth 0 ! -name 'Python' -exec rm -rf {} \; + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc /usr/local/.ghcup + sudo docker system prune -f + - uses: actions/checkout@v6 with: fetch-depth: 0 @@ -155,7 +171,7 @@ jobs: # install the latest pytorch for testing # however, "pip install monai*.tar.gz" will build cpp/cuda with an isolated # fresh torch installation according to pyproject.toml - python -m pip install torch>=2.5.1 torchvision + python -m pip install torch\>=2.5.1 torchvision - name: Check packages run: | pip uninstall monai @@ -213,6 +229,14 @@ jobs: build-docs: runs-on: ubuntu-latest steps: + - name: Clean unused tools + run: | + find /opt/hostedtoolcache/* -maxdepth 0 ! -name 'Python' -exec rm -rf {} \; + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc /usr/local/.ghcup + sudo docker system prune -f + - uses: actions/checkout@v6 - name: Set up Python 3.9 uses: actions/setup-python@v6 diff --git a/Dockerfile.slim b/Dockerfile.slim new file mode 100644 index 0000000000..6651269341 --- /dev/null +++ b/Dockerfile.slim @@ -0,0 +1,90 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# To build with a different base image +# please run `docker build` using the `--build-arg IMAGE=...` flag. +ARG IMAGE=debian:12-slim + +FROM ${IMAGE} AS build + +ARG TORCH_CUDA_ARCH_LIST="7.5 8.0 8.6 8.9 9.0+PTX" + +ENV DEBIAN_FRONTEND=noninteractive +ENV APT_INSTALL="apt install -y --no-install-recommends" + +RUN apt update && apt upgrade -y && \ + ${APT_INSTALL} ca-certificates python3-pip python-is-python3 git wget libopenslide0 unzip python3-dev && \ + wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb && \ + dpkg -i cuda-keyring_1.1-1_all.deb && \ + apt update && \ + ${APT_INSTALL} cuda-toolkit-12 && \ + rm -rf /usr/lib/python*/EXTERNALLY-MANAGED /var/lib/apt/lists/* && \ + python -m pip install --upgrade --no-cache-dir pip + +# TODO: remark for issue [revise the dockerfile](https://github.com/zarr-developers/numcodecs/issues/431) +RUN if [[ $(uname -m) =~ "aarch64" ]]; then \ + CFLAGS="-O3" DISABLE_NUMCODECS_SSE2=true DISABLE_NUMCODECS_AVX2=true python -m pip install numcodecs; \ + fi + +# NGC Client +WORKDIR /opt/tools +ARG NGC_CLI_URI="https://ngc.nvidia.com/downloads/ngccli_linux.zip" +RUN wget -q ${NGC_CLI_URI} && unzip ngccli_linux.zip && chmod u+x ngc-cli/ngc && \ + find ngc-cli/ -type f -exec md5sum {} + | LC_ALL=C sort | md5sum -c ngc-cli.md5 && \ + rm -rf ngccli_linux.zip ngc-cli.md5 + +WORKDIR /opt/monai + +# copy relevant parts of repo +COPY requirements.txt requirements-min.txt requirements-dev.txt versioneer.py setup.py setup.cfg pyproject.toml ./ +COPY LICENSE CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md README.md MANIFEST.in runtests.sh ./ +COPY tests ./tests +COPY monai ./monai + +# install full deps +RUN python -m pip install --no-cache-dir -r requirements-dev.txt + +# compile ext +RUN CUDA_HOME=/usr/local/cuda FORCE_CUDA=1 USE_COMPILED=1 BUILD_MONAI=1 python setup.py develop + +# recreate the image without the installed CUDA packages then copy the installed MONAI and Python directories +FROM ${IMAGE} AS build2 + +ENV DEBIAN_FRONTEND=noninteractive +ENV APT_INSTALL="apt install -y --no-install-recommends" + +RUN apt update && apt upgrade -y && \ + ${APT_INSTALL} ca-certificates python3-pip python-is-python3 git libopenslide0 && \ + apt clean && \ + rm -rf /usr/lib/python*/EXTERNALLY-MANAGED /var/lib/apt/lists/* && \ + python -m pip install --upgrade --no-cache-dir pip + +COPY --from=build /opt/monai /opt/monai +COPY --from=build /opt/tools /opt/tools +ARG PYTHON_VERSION=3.11 +COPY --from=build /usr/local/lib/python${PYTHON_VERSION}/dist-packages /usr/local/lib/python${PYTHON_VERSION}/dist-packages +COPY --from=build /usr/local/bin /usr/local/bin + +RUN rm -rf /opt/monai/build /opt/monai/monai.egg-info && \ + find / -name __pycache__ | xargs rm -rf + +# flatten all layers down to one +FROM ${IMAGE} +LABEL maintainer="monai.contact@gmail.com" + +COPY --from=build2 / / + +WORKDIR /opt/monai + +ENV PATH=${PATH}:/opt/tools:/opt/tools/ngc-cli +ENV POLYGRAPHY_AUTOINSTALL_DEPS=1 +ENV CUDA_HOME=/usr/local/cuda +ENV BUILD_MONAI=1 diff --git a/monai/apps/vista3d/inferer.py b/monai/apps/vista3d/inferer.py index 8f622ef6cd..bdd833af9d 100644 --- a/monai/apps/vista3d/inferer.py +++ b/monai/apps/vista3d/inferer.py @@ -86,13 +86,13 @@ def point_based_window_inferer( for j in range(len(ly_)): for k in range(len(lz_)): lx, rx, ly, ry, lz, rz = (lx_[i], rx_[i], ly_[j], ry_[j], lz_[k], rz_[k]) - unravel_slice = [ + unravel_slice = ( slice(None), slice(None), slice(int(lx), int(rx)), slice(int(ly), int(ry)), slice(int(lz), int(rz)), - ] + ) batch_image = image[unravel_slice] output = predictor( batch_image, diff --git a/monai/networks/nets/vista3d.py b/monai/networks/nets/vista3d.py index a5c2cc13ef..01aefd3416 100644 --- a/monai/networks/nets/vista3d.py +++ b/monai/networks/nets/vista3d.py @@ -243,14 +243,10 @@ def connected_components_combine( _logits = logits[mapping_index] inside = [] for i in range(_logits.shape[0]): - inside.append( - np.any( - [ - _logits[i, 0, p[0], p[1], p[2]].item() > 0 - for p in point_coords[i].cpu().numpy().round().astype(int) - ] - ) - ) + p_coord = point_coords[i].cpu().numpy().round().astype(int) + inside_p = [_logits[i, 0, p[0], p[1], p[2]].item() > 0 for p in p_coord] + inside.append(int(np.any(inside_p))) # convert to int to avoid typing problems with Numpy + inside_tensor = torch.tensor(inside).to(logits.device) nan_mask = torch.isnan(_logits) # _logits are converted to binary [B1, 1, H, W, D] diff --git a/monai/networks/utils.py b/monai/networks/utils.py index df91c84bdf..8d8484fb90 100644 --- a/monai/networks/utils.py +++ b/monai/networks/utils.py @@ -713,13 +713,13 @@ def convert_to_onnx( torch_versioned_kwargs = {} if use_trace: # let torch.onnx.export to trace the model. - mode_to_export = model + model_to_export = model torch_versioned_kwargs = kwargs if "dynamo" in kwargs and kwargs["dynamo"] and verify: torch_versioned_kwargs["verify"] = verify verify = False else: - mode_to_export = torch.jit.script(model, **kwargs) + model_to_export = torch.jit.script(model, **kwargs) if torch.is_tensor(inputs) or isinstance(inputs, dict): onnx_inputs = (inputs,) @@ -733,7 +733,7 @@ def convert_to_onnx( f = filename print(f"torch_versioned_kwargs={torch_versioned_kwargs}") torch.onnx.export( - mode_to_export, + model_to_export, onnx_inputs, f=f, input_names=input_names, diff --git a/requirements-dev.txt b/requirements-dev.txt index 1dc2141cf6..a295fda3e0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ # Full requirements for developments -r requirements-min.txt -pytorch-ignite==0.4.11 +pytorch-ignite gdown>=4.7.3 scipy>=1.12.0; python_version >= '3.9' itk>=5.2 @@ -52,8 +52,8 @@ nni==2.10.1; platform_system == "Linux" and "arm" not in platform_machine and "a optuna git+https://github.com/Project-MONAI/MetricsReloaded@monai-support#egg=MetricsReloaded onnx>=1.13.0 +onnxruntime onnxscript -onnxruntime; python_version <= '3.10' typeguard<3 # https://github.com/microsoft/nni/issues/5457 filelock<3.12.0 # https://github.com/microsoft/nni/issues/5523 zarr diff --git a/requirements.txt b/requirements.txt index c43fb21f92..40281a8ed2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -torch>=2.4.1; platform_system != "Windows" -torch>=2.4.1, !=2.7.0; platform_system == "Windows" +torch>=2.4.1, <2.9; platform_system != "Windows" +torch>=2.4.1, <2.9, !=2.7.0; platform_system == "Windows" numpy>=1.24,<3.0 diff --git a/tests/bundle/test_bundle_download.py b/tests/bundle/test_bundle_download.py index 650b0d7930..16c3407af6 100644 --- a/tests/bundle/test_bundle_download.py +++ b/tests/bundle/test_bundle_download.py @@ -15,7 +15,7 @@ import os import tempfile import unittest -from unittest.case import skipUnless +from unittest.case import skipIf, skipUnless from unittest.mock import patch import numpy as np @@ -219,6 +219,7 @@ def test_monaihosting_url_download_bundle(self, bundle_files, bundle_name, url): @parameterized.expand([TEST_CASE_5]) @skip_if_quick + @skipIf(os.getenv("NGC_API_KEY", None) is None, "NGC API key required for this test") def test_ngc_private_source_download_bundle(self, bundle_files, bundle_name, _url): with skip_if_downloading_fails(): # download a single file from url, also use `args_file` diff --git a/tests/data/meta_tensor/test_meta_tensor.py b/tests/data/meta_tensor/test_meta_tensor.py index 427902f784..c0e53fd24c 100644 --- a/tests/data/meta_tensor/test_meta_tensor.py +++ b/tests/data/meta_tensor/test_meta_tensor.py @@ -245,7 +245,7 @@ def test_pickling(self): with tempfile.TemporaryDirectory() as tmp_dir: fname = os.path.join(tmp_dir, "im.pt") torch.save(m, fname) - m2 = torch.load(fname, weights_only=True) + m2 = torch.load(fname, weights_only=False) self.check(m2, m, ids=False) @skip_if_no_cuda diff --git a/tests/losses/test_multi_scale.py b/tests/losses/test_multi_scale.py index 348a9f9913..87ccca7676 100644 --- a/tests/losses/test_multi_scale.py +++ b/tests/losses/test_multi_scale.py @@ -55,7 +55,7 @@ class TestMultiScale(unittest.TestCase): @parameterized.expand(TEST_CASES) def test_shape(self, input_param, input_data, expected_val): result = MultiScaleLoss(**input_param).forward(**input_data) - np.testing.assert_allclose(result.detach().cpu().numpy(), expected_val, rtol=1e-5) + np.testing.assert_allclose(result.detach().cpu().numpy(), expected_val, rtol=1e-4) @parameterized.expand( [ diff --git a/tests/networks/test_convert_to_onnx.py b/tests/networks/test_convert_to_onnx.py index 1d4cd6b071..74b91aaf31 100644 --- a/tests/networks/test_convert_to_onnx.py +++ b/tests/networks/test_convert_to_onnx.py @@ -22,10 +22,14 @@ from monai.networks.nets import SegResNet, UNet from tests.test_utils import SkipIfNoModule, optional_import, skip_if_quick -if torch.cuda.is_available(): - TORCH_DEVICE_OPTIONS = ["cpu", "cuda"] -else: - TORCH_DEVICE_OPTIONS = ["cpu"] +onnx, _ = optional_import("onnx") + +TORCH_DEVICE_OPTIONS = ["cpu"] + +# FIXME: CUDA seems to produce different model outputs during testing vs. ONNX outputs, use CPU only for now +# if torch.cuda.is_available(): +# TORCH_DEVICE_OPTIONS.append("cuda") + TESTS = list(itertools.product(TORCH_DEVICE_OPTIONS, [True, False], [True, False])) TESTS_ORT = list(itertools.product(TORCH_DEVICE_OPTIONS, [True])) @@ -35,14 +39,13 @@ else: rtol, atol = 1e-3, 1e-4 -onnx, _ = optional_import("onnx") - @SkipIfNoModule("onnx") @skip_if_quick class TestConvertToOnnx(unittest.TestCase): @parameterized.expand(TESTS) def test_unet(self, device, use_trace, use_ort): + """Test converting UNet to ONNX.""" if use_ort: _, has_onnxruntime = optional_import("onnxruntime") if not has_onnxruntime: @@ -50,23 +53,24 @@ def test_unet(self, device, use_trace, use_ort): model = UNet( spatial_dims=2, in_channels=1, out_channels=3, channels=(16, 32, 64), strides=(2, 2), num_res_units=0 ) - if use_trace: - onnx_model = convert_to_onnx( - model=model, - inputs=[torch.randn((16, 1, 32, 32), requires_grad=False)], - input_names=["x"], - output_names=["y"], - verify=True, - device=device, - use_ort=use_ort, - use_trace=use_trace, - rtol=rtol, - atol=atol, - ) - self.assertTrue(isinstance(onnx_model, onnx.ModelProto)) + + onnx_model = convert_to_onnx( + model=model, + inputs=[torch.randn((16, 1, 32, 32), requires_grad=False)], + input_names=["x"], + output_names=["y"], + verify=True, + device=device, + use_ort=use_ort, + use_trace=use_trace, + rtol=rtol, + atol=atol, + ) + self.assertTrue(isinstance(onnx_model, onnx.ModelProto)) @parameterized.expand(TESTS_ORT) def test_seg_res_net(self, device, use_ort): + """Test converting SetResNet to ONNX.""" if use_ort: _, has_onnxruntime = optional_import("onnxruntime") if not has_onnxruntime: