diff --git a/.github/workflows/cron-mmar.yml b/.github/workflows/cron-ngc-bundle.yml similarity index 77% rename from .github/workflows/cron-mmar.yml rename to .github/workflows/cron-ngc-bundle.yml index ae65388a8b..2b16d05ce7 100644 --- a/.github/workflows/cron-mmar.yml +++ b/.github/workflows/cron-ngc-bundle.yml @@ -1,15 +1,15 @@ -# daily tests for clara mmar models -name: cron-mmar +# daily tests for ngc bundles +name: cron-ngc-bundle on: - # schedule: - # - cron: "0 2 * * *" # at 02:00 UTC + schedule: + - cron: "0 2 * * *" # at 02:00 UTC # Allows you to run this workflow manually from the Actions tab workflow_dispatch: concurrency: # automatically cancel the previously triggered workflows when there's a newer version - group: mmar-tests-${{ github.event.pull_request.number || github.ref }} + group: bundle-tests-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: @@ -33,12 +33,12 @@ jobs: key: ${{ runner.os }}-pip-${{ steps.pip-cache.outputs.datew }} - name: Install dependencies run: | - rm -rf /github/home/.cache/torch/hub/mmars/ + rm -rf /github/home/.cache/torch/hub/bundle/ python -m pip install --upgrade pip wheel python -m pip install -r requirements-dev.txt - - name: Loading MMARs + - name: Loading Bundles run: | # clean up temporary files $(pwd)/runtests.sh --build --clean # run tests - python -m tests.ngc_mmar_loading + python -m tests.ngc_bundle_download diff --git a/.github/workflows/pythonapp-gpu.yml b/.github/workflows/pythonapp-gpu.yml index 9541bd1caa..47259be9aa 100644 --- a/.github/workflows/pythonapp-gpu.yml +++ b/.github/workflows/pythonapp-gpu.yml @@ -44,7 +44,7 @@ jobs: pytorch: "-h" # we explicitly set pytorch to -h to avoid pip install error base: "nvcr.io/nvidia/pytorch:22.09-py3" - environment: PT113+CUDA116 - pytorch: "torch==1.13.0 torchvision==0.14.0" + pytorch: "torch==1.13.1 torchvision==0.14.1" base: "nvcr.io/nvidia/cuda:11.6.1-devel-ubuntu18.04" container: image: ${{ matrix.base }} diff --git a/.github/workflows/pythonapp-min.yml b/.github/workflows/pythonapp-min.yml index 317fcbbcd2..a5e24639bf 100644 --- a/.github/workflows/pythonapp-min.yml +++ b/.github/workflows/pythonapp-min.yml @@ -52,11 +52,11 @@ jobs: - if: runner.os == 'windows' name: Install torch cpu from pytorch.org (Windows only) run: | - python -m pip install torch==1.13+cpu -f https://download.pytorch.org/whl/torch_stable.html + python -m pip install torch==1.13.1+cpu -f https://download.pytorch.org/whl/torch_stable.html - name: Install the dependencies run: | # min. requirements - python -m pip install torch==1.13 + python -m pip install torch==1.13.1 python -m pip install -r requirements-min.txt python -m pip list BUILD_MONAI=0 python setup.py develop # no compile of extensions diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index fb68e04b44..a603deb8a2 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -88,14 +88,14 @@ jobs: - if: runner.os == 'windows' name: Install torch cpu from pytorch.org (Windows only) run: | - python -m pip install torch==1.13.0+cpu torchvision==0.14.0+cpu -f https://download.pytorch.org/whl/torch_stable.html + python -m pip install torch==1.13.1+cpu torchvision==0.14.1+cpu -f https://download.pytorch.org/whl/torch_stable.html - if: runner.os == 'Linux' name: Install itk pre-release (Linux only) run: | python -m pip install --pre -U itk - name: Install the dependencies run: | - python -m pip install torch==1.13.0 torchvision==0.14.0 + python -m pip install torch==1.13.1 torchvision==0.14.1 cat "requirements-dev.txt" python -m pip install -r requirements-dev.txt python -m pip list diff --git a/.github/workflows/setupapp.yml b/.github/workflows/setupapp.yml index 7aa2649918..b8efda471e 100644 --- a/.github/workflows/setupapp.yml +++ b/.github/workflows/setupapp.yml @@ -100,7 +100,7 @@ jobs: - name: Install the dependencies run: | python -m pip install --upgrade pip wheel - python -m pip install torch==1.13.0 torchvision==0.14.0 + python -m pip install torch==1.13.1 torchvision==0.14.1 python -m pip install -r requirements-dev.txt - name: Run quick tests CPU ubuntu run: | diff --git a/CITATION.cff b/CITATION.cff index 9b8a9f6ec5..d00c8a364a 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -48,8 +48,6 @@ preferred-citation: family-names: "Xu" - given-names: "Ali" family-names: "Hatamizadeh" - - given-names: "Andriy" - family-names: "Myronenko" - given-names: "Wentao" family-names: "Zhu" - given-names: "Yun" diff --git a/README.md b/README.md index c3fd93f522..d54aa7e1a7 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,17 @@ **M**edical **O**pen **N**etwork for **AI** +![Supported Python versions](https://raw.githubusercontent.com/Project-MONAI/MONAI/dev/docs/images/python.svg) [![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)](https://opensource.org/licenses/Apache-2.0) -[![CI Build](https://github.com/Project-MONAI/MONAI/workflows/build/badge.svg?branch=dev)](https://github.com/Project-MONAI/MONAI/commits/dev) -[![Documentation Status](https://readthedocs.org/projects/monai/badge/?version=latest)](https://docs.monai.io/en/latest/?badge=latest) -[![codecov](https://codecov.io/gh/Project-MONAI/MONAI/branch/dev/graph/badge.svg?token=6FTC7U1JJ4)](https://codecov.io/gh/Project-MONAI/MONAI) [![PyPI version](https://badge.fury.io/py/monai.svg)](https://badge.fury.io/py/monai) -[![conda](https://img.shields.io/conda/vn/conda-forge/monai)](https://anaconda.org/conda-forge/monai) +[![docker](https://img.shields.io/badge/docker-pull-green.svg?logo=docker&logoColor=white)](https://hub.docker.com/r/projectmonai/monai) +[![conda](https://img.shields.io/conda/vn/conda-forge/monai?color=green)](https://anaconda.org/conda-forge/monai) + +[![premerge](https://github.com/Project-MONAI/MONAI/actions/workflows/pythonapp.yml/badge.svg?branch=dev)](https://github.com/Project-MONAI/MONAI/actions/workflows/pythonapp.yml) +[![postmerge](https://img.shields.io/github/checks-status/project-monai/monai/dev?label=postmerge)](https://github.com/Project-MONAI/MONAI/actions?query=branch%3Adev) +[![docker](https://github.com/Project-MONAI/MONAI/actions/workflows/docker.yml/badge.svg?branch=dev)](https://github.com/Project-MONAI/MONAI/actions/workflows/docker.yml) +[![Documentation Status](https://readthedocs.org/projects/monai/badge/?version=latest)](https://docs.monai.io/en/latest/) +[![codecov](https://codecov.io/gh/Project-MONAI/MONAI/branch/dev/graph/badge.svg?token=6FTC7U1JJ4)](https://codecov.io/gh/Project-MONAI/MONAI) MONAI is a [PyTorch](https://pytorch.org/)-based, [open-source](https://github.com/Project-MONAI/MONAI/blob/dev/LICENSE) framework for deep learning in healthcare imaging, part of [PyTorch Ecosystem](https://pytorch.org/ecosystem/). Its ambitions are: diff --git a/docs/images/exp_mgmt.png b/docs/images/exp_mgmt.png new file mode 100644 index 0000000000..f3faf4c09f Binary files /dev/null and b/docs/images/exp_mgmt.png differ diff --git a/docs/images/hovernet_diagram.png b/docs/images/hovernet_diagram.png new file mode 100644 index 0000000000..aa7adcbdcf Binary files /dev/null and b/docs/images/hovernet_diagram.png differ diff --git a/docs/images/python.svg b/docs/images/python.svg new file mode 100644 index 0000000000..2d24bd007f --- /dev/null +++ b/docs/images/python.svg @@ -0,0 +1 @@ +pythonpython3.7+3.7+ diff --git a/docs/source/whatsnew.rst b/docs/source/whatsnew.rst index bb6665e621..9dd00bcd1e 100644 --- a/docs/source/whatsnew.rst +++ b/docs/source/whatsnew.rst @@ -6,6 +6,7 @@ What's New .. toctree:: :maxdepth: 1 + whatsnew_1_1.md whatsnew_1_0.md whatsnew_0_9.md whatsnew_0_8.md diff --git a/docs/source/whatsnew_1_0.md b/docs/source/whatsnew_1_0.md index 36ab393af1..e8e0b031c1 100644 --- a/docs/source/whatsnew_1_0.md +++ b/docs/source/whatsnew_1_0.md @@ -1,4 +1,4 @@ -# What's new in 1.0 🎉🎉 +# What's new in 1.0 - Model Zoo - Auto3DSeg diff --git a/docs/source/whatsnew_1_1.md b/docs/source/whatsnew_1_1.md new file mode 100644 index 0000000000..261af460fc --- /dev/null +++ b/docs/source/whatsnew_1_1.md @@ -0,0 +1,75 @@ +# What's new in 1.1 🎉🎉 + +- Digital pathology workflows +- Experiment management for MONAI bundle +- Auto3dSeg enhancements +- New models in MONAI Model Zoo +- State-of-the-art SurgToolLoc solution + +## Digital pathology workflows + +![hovernet](../images/hovernet_diagram.png) + +Hover-Net is a model for simultaneous segmentation and classification of nuclei in multi-tissue histology images (Graham et al. Medical Image Analysis, 2019). +We have added support for this model in MONAI by implementing several new components, enhancing existing ones and providing pipelines and examples for training, validation and inference. + +Along with the modules release, new digital pathology analysis tutorials are made available: + +- [HoVerNet pipelines](https://github.com/Project-MONAI/tutorials/tree/main/pathology/hovernet) based on MONAI workflows for training, validation and inference +- [HoVerNet tutorial](https://github.com/Project-MONAI/tutorials/blob/main/pathology/hovernet/hovernet_torch.ipynb) for training, validation and inference +- NuClick (Interactive Annotation for Pathology) tutorials for [training](https://github.com/Project-MONAI/tutorials/blob/main/pathology/nuclick/nuclick_training_notebook.ipynb) +and [inference](https://github.com/Project-MONAI/tutorials/blob/main/pathology/nuclick/nuclick_infer.ipynb) +- Nuclei classification tutorials for [training](https://github.com/Project-MONAI/tutorials/blob/main/pathology/nuclick/nuclei_classification_training_notebook.ipynb) +and [inference](https://github.com/Project-MONAI/tutorials/blob/main/pathology/nuclick/nuclei_classification_infer.ipynb) + +## Experiment management for MONAI bundle + +![exp_mgmt](../images/exp_mgmt.png) + +In this release, experiment management features are integrated with MONAI bundle. +It provides essential APIs for managing the end-to-end model bundle lifecycle. +Users can start tracking experiments by, for example, appending `--tracking "mlflow"` to the training or inference commands to enable the MLFlow-based management. +By default, MLFlow will track the executed bundle config, model quality measurements, and source code versioning. +For more details, please refer to the [tutorial](https://github.com/Project-MONAI/tutorials/blob/main/experiment_management/bundle_integrate_mlflow.ipynb). + +## Auto3dSeg enhancements + +Multiple improvements have been added in `Auto3DSeg` both in terms of +usability and performance. +- Multi-modality support is added and applied for +automated segmentation of the HECKTOR22 challenge dataset, which includes input 3D +CT and PET images of various resolutions and sizes. A tutorial example of +running Auto3DSeg on the HECKTOR22 challenge dataset is available in MONAI +Tutorials. The tutorial is based on [the HECKTOR22 challenge 1st place solution](https://arxiv.org/abs/2209.10809). +- A new improved version of `Segresnet` Algo is now available in `AutoRunner`. +In this version, data caching is more efficient and the preprocessing transforms are more flexible. +The workflow progresses including the timings of steps are written to console output as well as a YAML file. +- Automatic customization and optimization of the model training configuration +can be achieved according to the GPU devices used. The feature +focuses on determining parameters including batch size of model +training and sliding-window inference, allocated devices for +data in sliding-window inference. For more details about how to enable it, please see [the tutorials](https://github.com/Project-MONAI/tutorials/tree/main/auto3dseg). + +## New models in MONAI Model Zoo + +New pretrained models are being created and released [in the Model Zoo](https://monai.io/model-zoo.html). +Notably, + +- The `mednist_reg` model demonstrates how to build image registration workflows in MONAI bundle +format. The model uses a ResNet and spatial transformer for hand X-ray image registration based on +[the registration_mednist tutorial](https://github.com/Project-MONAI/tutorials/blob/main/2d_registration/registration_mednist.ipynb), +- `pathology_nuclei_segmentation_and_classification`, + `pathology_nuclick_annotation`, and `pathology_nuclei_classification` bundles + are built for [digital pathology image + analysis](https://github.com/Project-MONAI/model-zoo/tree/dev/models/pathology_nuclei_segmentation_classification). + +For more details about how to use the models, please see [the tutorials](https://github.com/Project-MONAI/tutorials/tree/main/model_zoo). + +## State-of-the-art SurgToolLoc solution + +[SurgToolLoc](https://surgtoolloc.grand-challenge.org/Home/) is a part of the +[EndoVis](https://endovis.grand-challenge.org/) challenge at [MICCAI 2022](https://conferences.miccai.org/2022/en/). +The challenge focuses on endoscopic video analysis and is divided into (1) fully supervised tool classification +and (2) weakly supervised tool classification/localization. +Team NVIDIA won prizes by finishing [third](https://surgtoolloc.grand-challenge.org/results/) in both categories. +The core components of the solutions [are released in MONAI](https://github.com/Project-MONAI/tutorials/tree/main/competitions/MICCAI/surgtoolloc). diff --git a/monai/apps/auto3dseg/bundle_gen.py b/monai/apps/auto3dseg/bundle_gen.py index efbecb2061..59e90b795b 100644 --- a/monai/apps/auto3dseg/bundle_gen.py +++ b/monai/apps/auto3dseg/bundle_gen.py @@ -32,7 +32,7 @@ from monai.utils import ensure_tuple logger = get_logger(module_name=__name__) -ALGO_HASH = os.environ.get("MONAI_ALGO_HASH", "c812e5f") +ALGO_HASH = os.environ.get("MONAI_ALGO_HASH", "1dde7a1") __all__ = ["BundleAlgo", "BundleGen"] diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index cf9be1f98d..b625578049 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -25,6 +25,7 @@ import torch from torch.cuda import is_available +from monai.apps.mmars.mmars import _get_all_ngc_models from monai.apps.utils import _basename, download_url, extractall, get_logger from monai.bundle.config_item import ConfigComponent from monai.bundle.config_parser import ConfigParser @@ -42,6 +43,9 @@ logger = get_logger(module_name=__name__) +# set BUNDLE_DOWNLOAD_SRC="ngc" to use NGC source in default for bundle download +download_source = os.environ.get("BUNDLE_DOWNLOAD_SRC", "github") + def _update_args(args: Optional[Union[str, Dict]] = None, ignore_none: bool = True, **kwargs) -> Dict: """ @@ -130,9 +134,11 @@ def _get_git_release_url(repo_owner: str, repo_name: str, tag_name: str, filenam return f"https://github.com/{repo_owner}/{repo_name}/releases/download/{tag_name}/{filename}" +def _get_ngc_bundle_url(model_name: str, version: str): + return f"https://api.ngc.nvidia.com/v2/models/nvidia/monaitoolkit/{model_name}/versions/{version}/zip" + + def _download_from_github(repo: str, download_path: Path, filename: str, progress: bool = True): - if len(repo.split("/")) != 3: - raise ValueError("if source is `github`, repo should be in the form of `repo_owner/repo_name/release_tag`.") repo_owner, repo_name, tag_name = repo.split("/") if ".zip" not in filename: filename += ".zip" @@ -142,6 +148,45 @@ def _download_from_github(repo: str, download_path: Path, filename: str, progres extractall(filepath=filepath, output_dir=download_path, has_base=True) +def _add_ngc_prefix(name: str, prefix: str = "monai_"): + if name.startswith(prefix): + return name + return f"{prefix}{name}" + + +def _remove_ngc_prefix(name: str, prefix: str = "monai_"): + if name.startswith(prefix): + return name[len(prefix) :] + return name + + +def _download_from_ngc(download_path: Path, filename: str, version: str, remove_prefix: Optional[str], progress: bool): + # ensure prefix is contained + filename = _add_ngc_prefix(filename) + url = _get_ngc_bundle_url(model_name=filename, version=version) + filepath = download_path / f"{filename}_v{version}.zip" + if remove_prefix: + filename = _remove_ngc_prefix(filename, prefix=remove_prefix) + extract_path = download_path / f"{filename}" + download_url(url=url, filepath=filepath, hash_val=None, progress=progress) + extractall(filepath=filepath, output_dir=extract_path, has_base=True) + + +def _get_latest_bundle_version(source: str, name: str, repo: str): + if source == "ngc": + name = _add_ngc_prefix(name) + model_dict = _get_all_ngc_models(name) + for v in model_dict.values(): + if v["name"] == name: + return v["latest"] + return None + elif source == "github": + repo_owner, repo_name, tag_name = repo.split("/") + return get_bundle_versions(name, repo=os.path.join(repo_owner, repo_name), tag=tag_name)["latest_version"] + else: + raise ValueError(f"To get the latest bundle version, source should be 'github' or 'ngc', got {source}.") + + def _process_bundle_dir(bundle_dir: Optional[PathLike] = None): if bundle_dir is None: get_dir, has_home = optional_import("torch.hub", name="get_dir") @@ -156,9 +201,10 @@ def download( name: Optional[str] = None, version: Optional[str] = None, bundle_dir: Optional[PathLike] = None, - source: str = "github", - repo: str = "Project-MONAI/model-zoo/hosting_storage_v1", + source: str = download_source, + repo: Optional[str] = None, url: Optional[str] = None, + remove_prefix: Optional[str] = "monai_", progress: bool = True, args_file: Optional[str] = None, ): @@ -175,9 +221,12 @@ def download( # Execute this module as a CLI entry, and download bundle from the model-zoo repo: python -m monai.bundle download --name --version "0.1.0" --bundle_dir "./" - # Execute this module as a CLI entry, and download bundle: + # Execute this module as a CLI entry, and download bundle from specified github repo: python -m monai.bundle download --name --source "github" --repo "repo_owner/repo_name/release_tag" + # Execute this module as a CLI entry, and download bundle from ngc with latest version: + python -m monai.bundle download --name --source "ngc" --bundle_dir "./" + # Execute this module as a CLI entry, and download bundle via URL: python -m monai.bundle download --name --url @@ -190,18 +239,27 @@ def download( Args: name: bundle name. If `None` and `url` is `None`, it must be provided in `args_file`. - for example: "spleen_ct_segmentation", "prostate_mri_anatomy" in the model-zoo: + for example: + "spleen_ct_segmentation", "prostate_mri_anatomy" in model-zoo: https://github.com/Project-MONAI/model-zoo/releases/tag/hosting_storage_v1. - version: version name of the target bundle to download, like: "0.1.0". + "monai_brats_mri_segmentation" in ngc: + https://catalog.ngc.nvidia.com/models?filters=&orderBy=scoreDESC&query=monai. + version: version name of the target bundle to download, like: "0.1.0". If `None`, will download + the latest version. bundle_dir: target directory to store the downloaded data. Default is `bundle` subfolder under `torch.hub.get_dir()`. source: storage location name. This argument is used when `url` is `None`. - "github" is currently the only supported value. - repo: repo name. This argument is used when `url` is `None`. - If `source` is "github", it should be in the form of "repo_owner/repo_name/release_tag". + In default, the value is achieved from the environment variable BUNDLE_DOWNLOAD_SRC, and + it should be "ngc" or "github". + repo: repo name. This argument is used when `url` is `None` and `source` is "github". + If used, it should be in the form of "repo_owner/repo_name/release_tag". url: url to download the data. If not `None`, data will be downloaded directly and `source` will not be checked. If `name` is `None`, filename is determined by `monai.apps.utils._basename(url)`. + remove_prefix: This argument is used when `source` is "ngc". Currently, all ngc bundles + have the ``monai_`` prefix, which is not existing in their model zoo contrasts. In order to + maintain the consistency between these two sources, remove prefix is necessary. + Therefore, if specified, downloaded folder name will remove the prefix. progress: whether to display a progress bar. args_file: a JSON or YAML file to provide default values for all the args in this function. so that the command line inputs can be simplified. @@ -215,17 +273,20 @@ def download( source=source, repo=repo, url=url, + remove_prefix=remove_prefix, progress=progress, ) _log_input_summary(tag="download", args=_args) - source_, repo_, progress_, name_, version_, bundle_dir_, url_ = _pop_args( - _args, "source", "repo", "progress", name=None, version=None, bundle_dir=None, url=None + source_, progress_, remove_prefix_, repo_, name_, version_, bundle_dir_, url_ = _pop_args( + _args, "source", "progress", remove_prefix=None, repo=None, name=None, version=None, bundle_dir=None, url=None ) bundle_dir_ = _process_bundle_dir(bundle_dir_) - if name_ is not None and version_ is not None: - name_ = "_v".join([name_, version_]) + if repo_ is None: + repo_ = "Project-MONAI/model-zoo/hosting_storage_v1" + if len(repo_.split("/")) != 3: + raise ValueError("repo should be in the form of `repo_owner/repo_name/release_tag`.") if url_ is not None: if name_ is not None: @@ -234,14 +295,27 @@ def download( filepath = bundle_dir_ / f"{_basename(url_)}" download_url(url=url_, filepath=filepath, hash_val=None, progress=progress_) extractall(filepath=filepath, output_dir=bundle_dir_, has_base=True) - elif source_ == "github": - if name_ is None: - raise ValueError(f"To download from source: Github, `name` must be provided, got {name_}.") - _download_from_github(repo=repo_, download_path=bundle_dir_, filename=name_, progress=progress_) else: - raise NotImplementedError( - f"Currently only download from provided URL in `url` or Github is implemented, got source: {source_}." - ) + if name_ is None: + raise ValueError(f"To download from source: {source_}, `name` must be provided.") + if version_ is None: + version_ = _get_latest_bundle_version(source=source_, name=name_, repo=repo_) + if source_ == "github": + if version_ is not None: + name_ = "_v".join([name_, version_]) + _download_from_github(repo=repo_, download_path=bundle_dir_, filename=name_, progress=progress_) + elif source_ == "ngc": + _download_from_ngc( + download_path=bundle_dir_, + filename=name_, + version=version_, + remove_prefix=remove_prefix_, + progress=progress_, + ) + else: + raise NotImplementedError( + f"Currently only download from `url`, source 'github' or 'ngc' are implemented, got source: {source_}." + ) def load( @@ -250,8 +324,9 @@ def load( model_file: Optional[str] = None, load_ts_module: bool = False, bundle_dir: Optional[PathLike] = None, - source: str = "github", - repo: str = "Project-MONAI/model-zoo/hosting_storage_v1", + source: str = download_source, + repo: Optional[str] = None, + remove_prefix: Optional[str] = "monai_", progress: bool = True, device: Optional[str] = None, key_in_ckpt: Optional[str] = None, @@ -263,18 +338,29 @@ def load( Load model weights or TorchScript module of a bundle. Args: - name: bundle name, for example: "spleen_ct_segmentation", "prostate_mri_anatomy" in the model-zoo: + name: bundle name. If `None` and `url` is `None`, it must be provided in `args_file`. + for example: + "spleen_ct_segmentation", "prostate_mri_anatomy" in model-zoo: https://github.com/Project-MONAI/model-zoo/releases/tag/hosting_storage_v1. - version: version name of the target bundle to download, like: "0.1.0". + "monai_brats_mri_segmentation" in ngc: + https://catalog.ngc.nvidia.com/models?filters=&orderBy=scoreDESC&query=monai. + version: version name of the target bundle to download, like: "0.1.0". If `None`, will download + the latest version. model_file: the relative path of the model weights or TorchScript module within bundle. If `None`, "models/model.pt" or "models/model.ts" will be used. load_ts_module: a flag to specify if loading the TorchScript module. bundle_dir: directory the weights/TorchScript module will be loaded from. Default is `bundle` subfolder under `torch.hub.get_dir()`. source: storage location name. This argument is used when `model_file` is not existing locally and need to be - downloaded first. "github" is currently the only supported value. - repo: repo name. This argument is used when `model_file` is not existing locally and need to be - downloaded first. If `source` is "github", it should be in the form of "repo_owner/repo_name/release_tag". + downloaded first. + In default, the value is achieved from the environment variable BUNDLE_DOWNLOAD_SRC, and + it should be "ngc" or "github". + repo: repo name. This argument is used when `url` is `None` and `source` is "github". + If used, it should be in the form of "repo_owner/repo_name/release_tag". + remove_prefix: This argument is used when `source` is "ngc". Currently, all ngc bundles + have the ``monai_`` prefix, which is not existing in their model zoo contrasts. In order to + maintain the consistency between these two sources, remove prefix is necessary. + Therefore, if specified, downloaded folder name will remove the prefix. progress: whether to display a progress bar when downloading. device: target device of returned weights or module, if `None`, prefer to "cuda" if existing. key_in_ckpt: for nested checkpoint like `{"model": XXX, "optimizer": XXX, ...}`, specify the key of model @@ -298,9 +384,21 @@ def load( if model_file is None: model_file = os.path.join("models", "model.ts" if load_ts_module is True else "model.pt") + if source == "ngc": + name = _add_ngc_prefix(name) + if remove_prefix: + name = _remove_ngc_prefix(name, prefix=remove_prefix) full_path = os.path.join(bundle_dir_, name, model_file) if not os.path.exists(full_path): - download(name=name, version=version, bundle_dir=bundle_dir_, source=source, repo=repo, progress=progress) + download( + name=name, + version=version, + bundle_dir=bundle_dir_, + source=source, + repo=repo, + remove_prefix=remove_prefix, + progress=progress, + ) if device is None: device = "cuda:0" if is_available() else "cpu" @@ -421,7 +519,7 @@ def get_bundle_versions( bundles_info = _get_all_bundles_info(repo=repo, tag=tag, auth_token=auth_token) if bundle_name not in bundles_info: - raise ValueError(f"bundle: {bundle_name} is not existing.") + raise ValueError(f"bundle: {bundle_name} is not existing in repo: {repo}.") bundle_info = bundles_info[bundle_name] all_versions = sorted(bundle_info.keys()) diff --git a/setup.cfg b/setup.cfg index d14b7fe30b..30352d50db 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,6 +14,24 @@ project_urls = Documentation=https://docs.monai.io/ Bug Tracker=https://github.com/Project-MONAI/MONAI/issues Source Code=https://github.com/Project-MONAI/MONAI +classifiers = + Intended Audience :: Developers + Intended Audience :: Education + Intended Audience :: Science/Research + Intended Audience :: Healthcare Industry + Programming Language :: C++ + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Topic :: Scientific/Engineering + Topic :: Scientific/Engineering :: Artificial Intelligence + Topic :: Scientific/Engineering :: Medical Science Apps. + Topic :: Scientific/Engineering :: Information Analysis + Topic :: Software Development + Topic :: Software Development :: Libraries + Typing :: Typed [options] python_requires = >= 3.7 diff --git a/tests/ngc_mmar_loading.py b/tests/ngc_bundle_download.py similarity index 57% rename from tests/ngc_mmar_loading.py rename to tests/ngc_bundle_download.py index 8dca4f72c0..2b376c3c2d 100644 --- a/tests/ngc_mmar_loading.py +++ b/tests/ngc_bundle_download.py @@ -11,14 +11,81 @@ import os import sys +import tempfile import unittest import torch from parameterized import parameterized +from monai.apps import check_hash from monai.apps.mmars import MODEL_DESC, load_from_mmar +from monai.bundle import download, load from monai.config import print_debug_info from monai.networks.utils import copy_model_state +from tests.utils import assert_allclose, skip_if_downloading_fails, skip_if_quick, skip_if_windows + +TEST_CASE_NGC_1 = [ + "spleen_ct_segmentation", + "0.3.7", + None, + "monai_spleen_ct_segmentation", + "models/model.pt", + "b418a2dc8672ce2fd98dc255036e7a3d", +] +TEST_CASE_NGC_2 = [ + "monai_spleen_ct_segmentation", + "0.3.7", + "monai_", + "spleen_ct_segmentation", + "models/model.pt", + "b418a2dc8672ce2fd98dc255036e7a3d", +] + +TESTCASE_WEIGHTS = { + "key": "model.0.conv.unit0.adn.N.bias", + "value": torch.tensor( + [ + -0.0705, + -0.0937, + -0.0422, + -0.2068, + 0.1023, + -0.2007, + -0.0883, + 0.0018, + -0.1719, + 0.0116, + 0.0285, + -0.0044, + 0.1223, + -0.1287, + -0.1858, + 0.0460, + ] + ), +} + + +@skip_if_windows +class TestNgcBundleDownload(unittest.TestCase): + @parameterized.expand([TEST_CASE_NGC_1, TEST_CASE_NGC_2]) + @skip_if_quick + def test_ngc_download_bundle(self, bundle_name, version, remove_prefix, download_name, file_path, hash_val): + with skip_if_downloading_fails(): + with tempfile.TemporaryDirectory() as tempdir: + download( + name=bundle_name, source="ngc", version=version, bundle_dir=tempdir, remove_prefix=remove_prefix + ) + full_file_path = os.path.join(tempdir, download_name, file_path) + self.assertTrue(os.path.exists(full_file_path)) + self.assertTrue(check_hash(filepath=full_file_path, val=hash_val)) + + weights = load( + name=bundle_name, source="ngc", version=version, bundle_dir=tempdir, remove_prefix=remove_prefix + ) + assert_allclose( + weights[TESTCASE_WEIGHTS["key"]], TESTCASE_WEIGHTS["value"], atol=1e-4, rtol=1e-4, type_test=False + ) @unittest.skip("deprecating mmar tests") diff --git a/tests/test_auto3dseg_ensemble.py b/tests/test_auto3dseg_ensemble.py index 514844ff07..24cf37201e 100644 --- a/tests/test_auto3dseg_ensemble.py +++ b/tests/test_auto3dseg_ensemble.py @@ -49,11 +49,10 @@ train_param = ( { "CUDA_VISIBLE_DEVICES": list(range(num_gpus)), - "num_iterations": int(4 / num_gpus), - "num_iterations_per_validation": int(4 / num_gpus), "num_images_per_batch": 2, - "num_epochs": 1, - "num_warmup_iterations": int(4 / num_gpus), + "num_epochs": 2, + "num_epochs_per_validation": 1, + "num_warmup_epochs": 1, "use_pretrain": False, "pretrained_path": "", } diff --git a/tests/test_auto3dseg_hpo.py b/tests/test_auto3dseg_hpo.py index acb2721732..30c2361ef9 100644 --- a/tests/test_auto3dseg_hpo.py +++ b/tests/test_auto3dseg_hpo.py @@ -33,11 +33,10 @@ override_param = ( { "CUDA_VISIBLE_DEVICES": list(range(num_gpus)), - "num_iterations": int(4 / num_gpus), - "num_iterations_per_validation": int(4 / num_gpus), "num_images_per_batch": 2, - "num_epochs": 1, - "num_warmup_iterations": int(4 / num_gpus), + "num_epochs": 2, + "num_epochs_per_validation": 1, + "num_warmup_epochs": 1, "use_pretrain": False, "pretrained_path": "", } diff --git a/tests/test_bundle_download.py b/tests/test_bundle_download.py index 0bb7834dac..09cd0128f9 100644 --- a/tests/test_bundle_download.py +++ b/tests/test_bundle_download.py @@ -31,18 +31,16 @@ TEST_CASE_1 = ["test_bundle", None] -TEST_CASE_2 = ["test_bundle_v0.1.1", None] +TEST_CASE_2 = ["test_bundle", "0.1.1"] -TEST_CASE_3 = ["test_bundle", "0.1.1"] - -TEST_CASE_4 = [ +TEST_CASE_3 = [ ["model.pt", "model.ts", "network.json", "test_output.pt", "test_input.pt"], "test_bundle", "https://github.com/Project-MONAI/MONAI-extra-test-data/releases/download/0.8.1/test_bundle.zip", "a131d39a0af717af32d19e565b434928", ] -TEST_CASE_5 = [ +TEST_CASE_4 = [ ["model.pt", "model.ts", "network.json", "test_output.pt", "test_input.pt"], "test_bundle", "Project-MONAI/MONAI-extra-test-data/0.8.1", @@ -50,7 +48,7 @@ "model.pt", ] -TEST_CASE_6 = [ +TEST_CASE_5 = [ ["test_output.pt", "test_input.pt"], "test_bundle", "0.1.1", @@ -62,9 +60,9 @@ @skip_if_windows class TestDownload(unittest.TestCase): - @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3]) + @parameterized.expand([TEST_CASE_1, TEST_CASE_2]) @skip_if_quick - def test_download_bundle(self, bundle_name, version): + def test_github_download_bundle(self, bundle_name, version): bundle_files = ["model.pt", "model.ts", "network.json", "test_output.pt", "test_input.pt"] repo = "Project-MONAI/MONAI-extra-test-data/0.8.1" hash_val = "a131d39a0af717af32d19e565b434928" @@ -72,7 +70,7 @@ def test_download_bundle(self, bundle_name, version): # download a whole bundle from github releases with tempfile.TemporaryDirectory() as tempdir: cmd = ["coverage", "run", "-m", "monai.bundle", "download", "--name", bundle_name, "--source", "github"] - cmd += ["--bundle_dir", tempdir, "--repo", repo, "--progress", "False"] + cmd += ["--bundle_dir", tempdir, "--repo", repo] if version is not None: cmd += ["--version", version] command_line_tests(cmd) @@ -82,7 +80,7 @@ def test_download_bundle(self, bundle_name, version): if file == "network.json": self.assertTrue(check_hash(filepath=file_path, val=hash_val)) - @parameterized.expand([TEST_CASE_4]) + @parameterized.expand([TEST_CASE_3]) @skip_if_quick def test_url_download_bundle(self, bundle_files, bundle_name, url, hash_val): with skip_if_downloading_fails(): @@ -103,7 +101,7 @@ def test_url_download_bundle(self, bundle_files, bundle_name, url, hash_val): class TestLoad(unittest.TestCase): - @parameterized.expand([TEST_CASE_5]) + @parameterized.expand([TEST_CASE_4]) @skip_if_quick def test_load_weights(self, bundle_files, bundle_name, repo, device, model_file): with skip_if_downloading_fails(): @@ -150,7 +148,7 @@ def test_load_weights(self, bundle_files, bundle_name, repo, device, model_file) output_2 = model_2.forward(input_tensor) assert_allclose(output_2, expected_output, atol=1e-4, rtol=1e-4, type_test=False) - @parameterized.expand([TEST_CASE_6]) + @parameterized.expand([TEST_CASE_5]) @skip_if_quick @SkipIfBeforePyTorchVersion((1, 7, 1)) def test_load_ts_module(self, bundle_files, bundle_name, version, repo, device, model_file): diff --git a/tests/test_integration_autorunner.py b/tests/test_integration_autorunner.py index 4067aa1c6d..237045fda7 100644 --- a/tests/test_integration_autorunner.py +++ b/tests/test_integration_autorunner.py @@ -49,11 +49,10 @@ train_param = ( { "CUDA_VISIBLE_DEVICES": list(range(num_gpus)), - "num_iterations": int(4 / num_gpus), - "num_iterations_per_validation": int(4 / num_gpus), "num_images_per_batch": 2, - "num_epochs": 1, - "num_warmup_iterations": int(4 / num_gpus), + "num_epochs": 2, + "num_epochs_per_validation": 1, + "num_warmup_epochs": 1, "use_pretrain": False, "pretrained_path": "", } @@ -145,24 +144,21 @@ def test_autorunner_hpo(self) -> None: runner = AutoRunner(work_dir=work_dir, input=self.data_src_cfg, hpo=True, ensemble=False) hpo_param = { "CUDA_VISIBLE_DEVICES": train_param["CUDA_VISIBLE_DEVICES"], - "num_iterations": train_param["num_iterations"], - "num_iterations_per_validation": train_param["num_iterations_per_validation"], + "num_epochs_per_validation": train_param["num_epochs_per_validation"], "num_images_per_batch": train_param["num_images_per_batch"], "num_epochs": train_param["num_epochs"], - "num_warmup_iterations": train_param["num_warmup_iterations"], + "num_warmup_epochs": train_param["num_warmup_epochs"], "use_pretrain": train_param["use_pretrain"], "pretrained_path": train_param["pretrained_path"], # below are to shorten the time for dints - "training#num_iterations": train_param["num_iterations"], - "training#num_iterations_per_validation": train_param["num_iterations_per_validation"], + "training#num_epochs_per_validation": train_param["num_epochs_per_validation"], "training#num_images_per_batch": train_param["num_images_per_batch"], "training#num_epochs": train_param["num_epochs"], - "training#num_warmup_iterations": train_param["num_warmup_iterations"], - "searching#num_iterations": train_param["num_iterations"], - "searching#num_iterations_per_validation": train_param["num_iterations_per_validation"], + "training#num_warmup_epochs": train_param["num_warmup_epochs"], + "searching#num_epochs_per_validation": train_param["num_epochs_per_validation"], "searching#num_images_per_batch": train_param["num_images_per_batch"], "searching#num_epochs": train_param["num_epochs"], - "searching#num_warmup_iterations": train_param["num_warmup_iterations"], + "searching#num_warmup_epochs": train_param["num_warmup_epochs"], "nni_dry_run": True, } search_space = {"learning_rate": {"_type": "choice", "_value": [0.0001, 0.001, 0.01, 0.1]}} diff --git a/tests/test_integration_gpu_customization.py b/tests/test_integration_gpu_customization.py index c9e2cfec08..787c222c9f 100644 --- a/tests/test_integration_gpu_customization.py +++ b/tests/test_integration_gpu_customization.py @@ -49,11 +49,10 @@ train_param = ( { "CUDA_VISIBLE_DEVICES": list(range(num_gpus)), - "num_iterations": int(4 / num_gpus), - "num_iterations_per_validation": int(4 / num_gpus), "num_images_per_batch": 2, - "num_epochs": 1, - "num_warmup_iterations": int(4 / num_gpus), + "num_epochs": 2, + "num_epochs_per_validation": 1, + "num_warmup_epochs": 1, "use_pretrain": False, "pretrained_path": "", }