diff --git a/.github/workflows/cicd_tests.yml b/.github/workflows/cicd_tests.yml index d48e3a02f22..c2c6f05b5ae 100644 --- a/.github/workflows/cicd_tests.yml +++ b/.github/workflows/cicd_tests.yml @@ -148,16 +148,10 @@ jobs: python -m pip list BUILD_MONAI=0 python setup.py develop # no compile of extensions shell: bash - - if: matrix.os == 'linux-gpu-runner' - name: Print GPU Info - run: | - nvidia-smi - python -c 'import torch; print(torch.rand(2,2).to("cuda:0"))' - shell: bash - name: Run quick tests run: | - python -c 'import torch; print(torch.__version__); print(torch.rand(5,3))' - python -c "import monai; monai.config.print_config()" + nvidia-smi || true + python monai/config/check_env.py --env --monai ./runtests.sh --min shell: bash env: @@ -229,19 +223,18 @@ jobs: run: | python -m pip uninstall -y monai BUILD_MONAI=1 python -m pip install --no-build-isolation -e . # compile the cpp extensions + + nvidia-smi || true + python monai/config/check_env.py --env --monai shell: bash - if: runner.os != 'macOS' name: Run full tests run: | - python -c 'import torch; print(torch.__version__); print(torch.rand(5,3))' - python -c "import monai; monai.config.print_config()" python -m unittest -v shell: bash - if: runner.os == 'macOS' name: Run min tests run: | - python -c 'import torch; print(torch.__version__); print(torch.rand(5,3))' - python -c "import monai; monai.config.print_config()" # TODO: enable large range of macOS tests which don't take a very long time ./runtests.sh --min shell: bash @@ -351,8 +344,7 @@ jobs: run: | # install from wheel python -m pip install monai*.whl --extra-index-url https://download.pytorch.org/whl/cpu - python -c 'import monai; monai.config.print_config()' 2>&1 | grep -iv "unknown" - python -c 'import monai; print(monai.__file__)' + python -m monai.config python -m pip uninstall -y monai rm monai*.whl - name: Install source archive @@ -361,5 +353,4 @@ jobs: for name in *.tar.gz; do break; done echo $name python -m pip install ${name}[all] --extra-index-url https://download.pytorch.org/whl/cpu - python -c 'import monai; monai.config.print_config()' 2>&1 | grep -iv "unknown" - python -c 'import monai; print(monai.__file__)' + python -m monai.config diff --git a/MANIFEST.in b/MANIFEST.in index 66650af6bee..aac0c2ccaec 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include versioneer.py include monai/_version.py +include monai/config/check_env.py include README.md include LICENSE diff --git a/monai/config/__init__.py b/monai/config/__init__.py index c814e1f8ebc..a83889aee00 100644 --- a/monai/config/__init__.py +++ b/monai/config/__init__.py @@ -14,7 +14,6 @@ from .deviceconfig import ( USE_COMPILED, USE_META_DICT, - IgniteInfo, get_config_values, get_gpu_info, get_optional_config_values, diff --git a/monai/config/__main__.py b/monai/config/__main__.py new file mode 100644 index 00000000000..13b1cc9d512 --- /dev/null +++ b/monai/config/__main__.py @@ -0,0 +1,15 @@ +# 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. + +if __name__ == "__main__": + import monai + + monai.config.print_debug_info() # type: ignore diff --git a/monai/config/check_env.py b/monai/config/check_env.py new file mode 100644 index 00000000000..0dcb8fb3a79 --- /dev/null +++ b/monai/config/check_env.py @@ -0,0 +1,181 @@ +#! /usr/bin/env python + +# 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. + +""" +Script for checking various elements of the runtime environment and printing a large amount of diagnostic information. + +This is meant to be used for debugging environments used with MONAI, but doesn't directly need MONAI itself. It will +print information about the environment, including trying to get installed packages, test PyTorch with CUDA, and then +have MONAI print its debugging information if no errors encountered. If MONAI is not installed this script should still +work and produce useful information. Only standard libraries are needed in case a bare environment is being used. + +This can be run as a program with the following options to see all outputs: + + python check_env.py --env --envvars --monai + +With no options at all this can be used to test that PyTorch is installed and can move a tensor to a device. This is +useful when creating a fresh test environment and MONAI isn't present yet but it's good practice to valid PyTorch. + +This can also be used remotely with only Python installed to get current environment diagnostic info: + + curl https://raw.githubusercontent.com/Project-MONAI/MONAI/refs/heads/dev/monai/config/check_env.py | python +""" + +from __future__ import annotations + +import argparse +import getpass +import multiprocessing +import os +import platform +import shutil +import subprocess +import sys +from functools import partial + +DESC = """ +Script for checking various elements of the runtime environment and printing a large amount of diagnostic information. +This is used for debugging your environment by printing out various system statistics and diagnostic information. It +checks PyTorch and MONAI are installed and functioning. A typical use case is with the `--env` and `--monai` options. +""" + +USER = getpass.getuser() +HOST = platform.node() +efprint = partial(print, flush=True, file=sys.stderr) + + +def fprint(*args, **kwargs): + """ + Print with flushing, replacing the username and hostname values with placeholders for better anonymization. + """ + kwargs["flush"] = True + content = " ".join(map(str, args)) + content = content.replace(USER, "").replace(HOST, "") + print(content, **kwargs) + + +def print_platform(): + """ + Print basic platform information. + """ + fprint(platform.platform()) + fprint("uname:", list(platform.uname())) + fprint("CPU:", platform.processor(), "Count:", multiprocessing.cpu_count()) + fprint("Python:", sys.executable, platform.python_implementation(), platform.python_version()) + + +def print_environment_vars(): + """ + Print all environment variables other than a few known pointless ones. + """ + fprint("Environment:") + for k, v in os.environ.items(): + if k not in ("LS_COLORS", "PS1", "PS2"): + fprint(f" {k}:", v) + + +def print_environment(): + """ + Print the installed environment using `conda` or `pip`, fail if neither are present. + """ + try: + cmd = ("conda", "env", "export") if shutil.which("conda") else ("pip", "list") + + result = subprocess.check_output(cmd, stderr=subprocess.STDOUT) + fprint(result.decode()) + return True + except Exception as e: + efprint(f"Exception encountered getting environment with conda/pip: {e}") + return False + + +def check_torch(): + """ + Check PyTorch is installed and a tensor can be created. + """ + try: + import torch + + t = torch.rand(2, 3) * 5 + fprint("PyTorch:", torch.__version__, torch.__path__) + fprint("Test tensor:", t.flatten()) + return True + except ImportError: + efprint("PyTorch not installed") + return False + + +def check_torch_cuda(): + """ + Check CUDA capability in PyTorch by moving a tensor to each available device. + """ + import torch + + dcount = torch.cuda.device_count() + fprint("CUDA version:", torch.version.cuda) + fprint("PyTorch GPU Count:", dcount) + + try: + for d in range(dcount): + fprint(f" {torch.cuda.get_device_properties(d)}") + t = torch.rand(2, 3).to(torch.device(f"cuda:{d}")) * 5 + fprint("Test tensor:", t.flatten()) + return True + except Exception as e: + efprint(f"PyTorch encountered exception creating GPU tensor on device {d}: {e}") + return False + + +def check_monai(): + """ + Check MONAI by importing it then printing its debug info. + """ + try: + import monai + + monai.config.print_debug_info() # type: ignore + return True + except ImportError: + efprint("MONAI not installed") + return False + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(prog="check_env.py", description=DESC.strip()) + parser.add_argument("--env", default=False, action="store_true", help="Print environment info") + parser.add_argument("--envvars", default=False, action="store_true", help="Include environment variables") + parser.add_argument("--monai", default=False, action="store_true", help="Print MONAI info") + args = parser.parse_args() + + fprint("=" * 10, "Platform Info", "=" * 10) + print_platform() + + if args.env: + fprint("=" * 10, "Checking Environment", "=" * 10) + if args.envvars: + print_environment_vars() + print_environment() + + fprint("=" * 10, "Checking PyTorch", "=" * 10) + + if not check_torch(): + efprint("Exiting early, no valid PyTorch install found.") + sys.exit(1) + + if not check_torch_cuda(): + efprint("Exiting early, PyTorch encountered CUDA error.") + sys.exit(1) + + if args.monai: + fprint("=" * 10, "Checking MONAI", "=" * 10) + check_monai() diff --git a/monai/config/deviceconfig.py b/monai/config/deviceconfig.py index 5800651e59a..6a0d2378aa7 100644 --- a/monai/config/deviceconfig.py +++ b/monai/config/deviceconfig.py @@ -23,8 +23,6 @@ import torch import monai -from monai.utils.deprecate_utils import deprecated -from monai.utils.enums import IgniteInfo as _IgniteInfo from monai.utils.module import OptionalImportError, get_package_version, optional_import try: @@ -47,9 +45,21 @@ "print_debug_info", "USE_COMPILED", "USE_META_DICT", - "IgniteInfo", ] +USER = getpass.getuser() +HOST = platform.node() + + +def fprint(*args, **kwargs): + """ + Print with flushing, replacing the username and hostname values with placeholders for better anonymization. + """ + kwargs["flush"] = True + content = " ".join(map(str, args)) + content = content.replace(USER, "").replace(HOST, "") + print(content, **kwargs) + def get_config_values(): """ @@ -100,17 +110,15 @@ def print_config(file=sys.stdout): file: `print()` text stream file. Defaults to `sys.stdout`. """ for k, v in get_config_values().items(): - print(f"{k} version: {v}", file=file, flush=True) - print(f"MONAI flags: HAS_EXT = {HAS_EXT}, USE_COMPILED = {USE_COMPILED}, USE_META_DICT = {USE_META_DICT}") - print(f"MONAI rev id: {monai.__revision_id__}") - username = getpass.getuser() - masked_file_path = re.sub(username, "", monai.__file__) - print(f"MONAI __file__: {masked_file_path}", file=file, flush=True) - print("\nOptional dependencies:", file=file, flush=True) + fprint(f"{k} version: {v}", file=file) + fprint(f"MONAI flags: {HAS_EXT=}, {USE_COMPILED=}, {USE_META_DICT=}", file=file) + fprint(f"MONAI rev id: {monai.__revision_id__}", file=file) + fprint(f"MONAI __file__: {monai.__file__}", file=file) + fprint("\nOptional dependencies:", file=file) for k, v in get_optional_config_values().items(): - print(f"{k} version: {v}", file=file, flush=True) - print("\nFor details about installing the optional dependencies, please visit:", file=file, flush=True) - print( + fprint(f"{k} version: {v}", file=file) + fprint("\nFor details about installing the optional dependencies, please visit:", file=file) + fprint( " https://monai.readthedocs.io/en/latest/installation.html#installing-the-recommended-dependencies\n", file=file, flush=True, @@ -191,10 +199,10 @@ def print_system_info(file: TextIO = sys.stdout) -> None: file: `print()` text stream file. Defaults to `sys.stdout`. """ if not has_psutil: - print("`psutil` required for `print_system_info`", file=file, flush=True) + fprint("`psutil` required for `print_system_info`", file=file) else: for k, v in get_system_info().items(): - print(f"{k}: {v}", file=file, flush=True) + fprint(f"{k}: {v}", file=file) def get_gpu_info() -> OrderedDict: @@ -239,7 +247,7 @@ def print_gpu_info(file: TextIO = sys.stdout) -> None: file: `print()` text stream file. Defaults to `sys.stdout`. """ for k, v in get_gpu_info().items(): - print(f"{k}: {v}", file=file, flush=True) + fprint(f"{k}: {v}", file=file) def print_debug_info(file: TextIO = sys.stdout) -> None: @@ -249,26 +257,19 @@ def print_debug_info(file: TextIO = sys.stdout) -> None: Args: file: `print()` text stream file. Defaults to `sys.stdout`. """ - print("================================", file=file, flush=True) - print("Printing MONAI config...", file=file, flush=True) - print("================================", file=file, flush=True) + fprint("================================", file=file) + fprint("Printing MONAI config...", file=file) + fprint("================================", file=file) print_config(file) - print("\n================================", file=file, flush=True) - print("Printing system config...") - print("================================", file=file, flush=True) + fprint("\n================================", file=file) + fprint("Printing system config...", file=file) + fprint("================================", file=file) print_system_info(file) - print("\n================================", file=file, flush=True) - print("Printing GPU config...") - print("================================", file=file, flush=True) + fprint("\n================================", file=file) + fprint("Printing GPU config...", file=file) + fprint("================================", file=file) print_gpu_info(file) -@deprecated(since="1.4.0", removed="1.6.0", msg_suffix="Please use `monai.utils.enums.IgniteInfo` instead.") -class IgniteInfo: - """Deprecated Import of IgniteInfo enum, which was moved to `monai.utils.enums.IgniteInfo`.""" - - OPT_IMPORT_VERSION = _IgniteInfo.OPT_IMPORT_VERSION - - if __name__ == "__main__": print_debug_info() diff --git a/tests/config/test_print_info.py b/tests/config/test_print_info.py index aa152e183c0..da91c431498 100644 --- a/tests/config/test_print_info.py +++ b/tests/config/test_print_info.py @@ -12,6 +12,7 @@ from __future__ import annotations import unittest +from io import StringIO from monai.config import print_debug_info @@ -19,7 +20,9 @@ class TestPrintInfo(unittest.TestCase): def test_print_info(self): - print_debug_info() + out = StringIO() + print_debug_info(file=out) + self.assertGreater(out.tell(), 0) if __name__ == "__main__":