{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Deploying a MedNIST Classifier App with MONAI Deploy App SDK\n", "\n", "This tutorial demos the process of packaging up a trained model using MONAI Deploy App SDK into an artifact which can be run as a local program performing inference, a workflow job doing the same, and a Docker containerized workflow execution.\n", "\n", "In this tutorial, we will train a MedNIST classifier like the [MONAI tutorial here](https://github.com/Project-MONAI/tutorials/blob/master/2d_classification/mednist_tutorial.ipynb) and then implement & package the inference application, executing the application locally.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Train a MedNIST classifier model with MONAI Core" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Setup environment" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/ignite/handlers/checkpoint.py:17: DeprecationWarning: `TorchScript` support for functional optimizers is deprecated and will be removed in a future PyTorch release. Consider using the `torch.compile` optimizer instead.\n", " from torch.distributed.optim import ZeroRedundancyOptimizer\n" ] } ], "source": [ "# Install necessary packages for MONAI Core\n", "!python -c \"import monai\" || pip install -q \"monai[pillow, tqdm]\"\n", "!python -c \"import ignite\" || pip install -q \"monai[ignite]\"\n", "!python -c \"import gdown\" || pip install -q \"monai[gdown]\"\n", "!python -c \"import pydicom\" || pip install -q \"pydicom>=1.4.2\"\n", "!python -c \"import highdicom\" || pip install -q \"highdicom>=0.18.2\" # for the use of DICOM Writer operators\n", "\n", "# Install MONAI Deploy App SDK package\n", "!python -c \"import monai.deploy\" || pip install -q \"monai-deploy-app-sdk\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Setup imports" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "MONAI version: 1.4.0\n", "Numpy version: 1.26.4\n", "Pytorch version: 2.5.1+cu124\n", "MONAI flags: HAS_EXT = False, USE_COMPILED = False, USE_META_DICT = False\n", "MONAI rev id: 46a5272196a6c2590ca2589029eed8e4d56ff008\n", "MONAI __file__: /home//src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/monai/__init__.py\n", "\n", "Optional dependencies:\n", "Pytorch Ignite version: 0.4.11\n", "ITK version: NOT INSTALLED or UNKNOWN VERSION.\n", "Nibabel version: 5.3.2\n", "scikit-image version: 0.25.1\n", "scipy version: 1.15.1\n", "Pillow version: 11.1.0\n", "Tensorboard version: NOT INSTALLED or UNKNOWN VERSION.\n", "gdown version: 5.2.0\n", "TorchVision version: NOT INSTALLED or UNKNOWN VERSION.\n", "tqdm version: 4.67.1\n", "lmdb version: NOT INSTALLED or UNKNOWN VERSION.\n", "psutil version: 6.1.1\n", "pandas version: NOT INSTALLED or UNKNOWN VERSION.\n", "einops version: NOT INSTALLED or UNKNOWN VERSION.\n", "transformers version: NOT INSTALLED or UNKNOWN VERSION.\n", "mlflow version: NOT INSTALLED or UNKNOWN VERSION.\n", "pynrrd version: NOT INSTALLED or UNKNOWN VERSION.\n", "clearml version: NOT INSTALLED or UNKNOWN VERSION.\n", "\n", "For details about installing the optional dependencies, please visit:\n", " https://docs.monai.io/en/latest/installation.html#installing-the-recommended-dependencies\n", "\n" ] } ], "source": [ "# Copyright 2020 MONAI Consortium\n", "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", "# you may not use this file except in compliance with the License.\n", "# You may obtain a copy of the License at\n", "# http://www.apache.org/licenses/LICENSE-2.0\n", "# Unless required by applicable law or agreed to in writing, software\n", "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", "# See the License for the specific language governing permissions and\n", "# limitations under the License.\n", "\n", "import os\n", "import shutil\n", "import tempfile\n", "import glob\n", "import PIL.Image\n", "import torch\n", "import numpy as np\n", "\n", "from ignite.engine import Events\n", "\n", "from monai.apps import download_and_extract\n", "from monai.config import print_config\n", "from monai.networks.nets import DenseNet121\n", "from monai.engines import SupervisedTrainer\n", "from monai.transforms import (\n", " EnsureChannelFirst,\n", " Compose,\n", " LoadImage,\n", " RandFlip,\n", " RandRotate,\n", " RandZoom,\n", " ScaleIntensity,\n", " EnsureType,\n", ")\n", "from monai.utils import set_determinism\n", "\n", "set_determinism(seed=0)\n", "\n", "print_config()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Download dataset\n", "\n", "The MedNIST dataset was gathered from several sets from [TCIA](https://wiki.cancerimagingarchive.net/display/Public/Data+Usage+Policies+and+Restrictions),\n", "the RSNA Bone Age Challenge(https://www.rsna.org/education/ai-resources-and-training/ai-image-challenge/rsna-pediatric-bone-age-challenge-2017),\n", "and [the NIH Chest X-ray dataset](https://cloud.google.com/healthcare/docs/resources/public-datasets/nih-chest).\n", "\n", "The dataset is kindly made available by [Dr. Bradley J. Erickson M.D., Ph.D.](https://www.mayo.edu/research/labs/radiology-informatics/overview) (Department of Radiology, Mayo Clinic)\n", "under the Creative Commons [CC BY-SA 4.0 license](https://creativecommons.org/licenses/by-sa/4.0/).\n", "\n", "If you use the MedNIST dataset, please acknowledge the source.\n", "\n", "**_Note:_** Data files are now access controlled. Please first request permission to access the [shared folder on Google Drive](https://drive.google.com/drive/folders/1Z9s3JB2YdKjcw8ELwjVYJcCEvqlQ_HTN?usp=drive_link), then download and extract the zip file to a folder called `MedNIST_DATA` at the same level as this notebook." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "./MedNIST_DATA\n" ] } ], "source": [ "directory = os.environ.get(\"MONAI_DATA_DIRECTORY\")\n", "root_dir = directory if directory else os.path.join(os.path.curdir, \"MedNIST_DATA\")\n", "print(root_dir)\n", "\n", "resource = \"https://drive.google.com/uc?id=1QsnnkvZyJPcbRoV_ArW8SnE1OTuoVbKE\"\n", "md5 = \"0bc7306e7427e00ad1c5526a6677552d\"\n", "\n", "compressed_file = os.path.join(root_dir, \"MedNIST.tar.gz\")\n", "data_dir = os.path.join(root_dir, \"MedNIST\")\n", "if not os.path.exists(data_dir):\n", " download_and_extract(resource, compressed_file, root_dir, md5)" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Label names: ['AbdomenCT', 'BreastMRI', 'CXR', 'ChestCT', 'Hand', 'HeadCT']\n", "Label counts: [10000, 8954, 10000, 10000, 10000, 10000]\n", "Total image count: 58954\n", "Image dimensions: 64 x 64\n" ] } ], "source": [ "subdirs = sorted(glob.glob(f\"{data_dir}/*/\"))\n", "\n", "class_names = [os.path.basename(sd[:-1]) for sd in subdirs]\n", "image_files = [glob.glob(f\"{sb}/*\") for sb in subdirs]\n", "\n", "image_files_list = sum(image_files, [])\n", "image_class = sum(([i] * len(f) for i, f in enumerate(image_files)), [])\n", "image_width, image_height = PIL.Image.open(image_files_list[0]).size\n", "\n", "print(f\"Label names: {class_names}\")\n", "print(f\"Label counts: {list(map(len, image_files))}\")\n", "print(f\"Total image count: {len(image_class)}\")\n", "print(f\"Image dimensions: {image_width} x {image_height}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Setup and train\n", "\n", "Here we'll create a transform sequence and train the network, omitting validation and testing since we know this does indeed work and it's not needed here:\n", "\n", "(train_transforms)=" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "train_transforms = Compose(\n", " [\n", " LoadImage(image_only=True),\n", " EnsureChannelFirst(channel_dim=\"no_channel\"),\n", " ScaleIntensity(),\n", " RandRotate(range_x=np.pi / 12, prob=0.5, keep_size=True),\n", " RandFlip(spatial_axis=0, prob=0.5),\n", " RandZoom(min_zoom=0.9, max_zoom=1.1, prob=0.5),\n", " EnsureType(),\n", " ]\n", ")" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "class MedNISTDataset(torch.utils.data.Dataset):\n", " def __init__(self, image_files, labels, transforms):\n", " self.image_files = image_files\n", " self.labels = labels\n", " self.transforms = transforms\n", "\n", " def __len__(self):\n", " return len(self.image_files)\n", "\n", " def __getitem__(self, index):\n", " return self.transforms(self.image_files[index]), self.labels[index]\n", "\n", "\n", "# just one dataset and loader, we won't bother with validation or testing \n", "train_ds = MedNISTDataset(image_files_list, image_class, train_transforms)\n", "train_loader = torch.utils.data.DataLoader(train_ds, batch_size=300, shuffle=True, num_workers=10)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", "net = DenseNet121(spatial_dims=2, in_channels=1, out_channels=len(class_names)).to(device)\n", "loss_function = torch.nn.CrossEntropyLoss()\n", "opt = torch.optim.Adam(net.parameters(), 1e-5)\n", "max_epochs = 5" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Epoch 1/5 Loss: 0.18886765837669373\n", "Epoch 2/5 Loss: 0.06690701842308044\n", "Epoch 3/5 Loss: 0.028753578662872314\n", "Epoch 4/5 Loss: 0.019015837460756302\n", "Epoch 5/5 Loss: 0.0193385761231184\n" ] } ], "source": [ "def _prepare_batch(batch, device, non_blocking):\n", " return tuple(b.to(device) for b in batch)\n", "\n", "\n", "trainer = SupervisedTrainer(device, max_epochs, train_loader, net, opt, loss_function, prepare_batch=_prepare_batch)\n", "\n", "\n", "@trainer.on(Events.EPOCH_COMPLETED)\n", "def _print_loss(engine):\n", " print(f\"Epoch {engine.state.epoch}/{engine.state.max_epochs} Loss: {engine.state.output[0]['loss']}\")\n", "\n", "\n", "trainer.run()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The network will be saved out here as a Torchscript object named `classifier.zip`" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "torch.jit.script(net).save(\"classifier.zip\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Implementing and Packaging Application with MONAI Deploy App SDK\n", "\n", "Based on the Torchscript model(`classifier.zip`), we will implement an application that process an input Jpeg image and write the prediction(classification) result as JSON file(`output.json`).\n", "\n", "### Creating Operators and connecting them in Application class\n", "\n", "We used the following [train transforms](train_transforms) as pre-transforms during the training.\n", "\n", "```{code-block} python\n", "---\n", "lineno-start: 1\n", "emphasize-lines: 3,4,5,9\n", "caption: |\n", " Train transforms used in training\n", "---\n", "train_transforms = Compose(\n", " [\n", " LoadImage(image_only=True),\n", " EnsureChannelFirst(channel_dim=\"no_channel\"),\n", " ScaleIntensity(),\n", " RandRotate(range_x=np.pi / 12, prob=0.5, keep_size=True),\n", " RandFlip(spatial_axis=0, prob=0.5),\n", " RandZoom(min_zoom=0.9, max_zoom=1.1, prob=0.5),\n", " EnsureType(),\n", " ]\n", ")\n", "```\n", "\n", "`RandRotate`, `RandFlip`, and `RandZoom` transforms are used only for training and those are not necessary during the inference.\n", "\n", "In our inference application, we will define two operators:\n", "\n", "1. `LoadPILOperator` - Load a JPEG image from the input path and pass the loaded image object to the next operator.\n", " - This Operator does similar job with `LoadImage(image_only=True)` transform in *train_transforms*, but handles only one image.\n", " - **Input**: a file path (`Path`)\n", " - **Output**: an image object in memory ([`Image`](/modules/_autosummary/monai.deploy.core.domain.Image))\n", "2. `MedNISTClassifierOperator` - Pre-transform the given image by using MONAI's `Compose` class, feed to the Torchscript model (`classifier.zip`), and write the prediction into JSON file(`output.json`)\n", " - Pre-transforms consist of three transforms -- `EnsureChannelFirst`, `ScaleIntensity`, and `EnsureType`.\n", " - **Input**: an image object in memory ([`Image`](/modules/_autosummary/monai.deploy.core.domain.Image))\n", " - **Output**: a folder path that the prediction result(`output.json`) would be written (`DataPath`)\n", "\n", "The workflow of the application would look like this.\n", "\n", "```{mermaid}\n", "%%{init: {\"theme\": \"base\", \"themeVariables\": { \"fontSize\": \"16px\"}} }%%\n", "\n", "classDiagram\n", " direction LR\n", "\n", " LoadPILOperator --|> MedNISTClassifierOperator : image...image\n", "\n", "\n", " class LoadPILOperator {\n", " image : DISK\n", " image(out) IN_MEMORY\n", " }\n", " class MedNISTClassifierOperator {\n", " image : IN_MEMORY\n", " output(out) DISK\n", " }\n", "```\n", "\n", "\n", "#### Set up environment variables\n", "\n", "Before proceeding to the application building and packaging, we first need to set the well-known environment variables, because the application parses them for the input, output, and model folders. Defaults are used if these environment variable are absent.\n", "\n", "Set the environment variables corresponding to the extracted data path." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "001420.jpeg\n", "classifier.zip\n", "env: HOLOSCAN_INPUT_PATH=input\n", "env: HOLOSCAN_OUTPUT_PATH=output\n", "env: HOLOSCAN_MODEL_PATH=models\n" ] } ], "source": [ "input_folder = \"input\"\n", "output_foler = \"output\"\n", "models_folder = \"models\"\n", "\n", "# Choose a file as test input\n", "test_input_path = image_files[0][0]\n", "!rm -rf {input_folder} && mkdir -p {input_folder} && cp {test_input_path} {input_folder} && ls {input_folder}\n", "# Need to copy the model file to its own clean subfolder for packaging, to workaround an issue in the Packager\n", "!rm -rf {models_folder} && mkdir -p {models_folder}/model && cp classifier.zip {models_folder}/model && ls {models_folder}/model\n", "\n", "%env HOLOSCAN_INPUT_PATH {input_folder}\n", "%env HOLOSCAN_OUTPUT_PATH {output_foler}\n", "%env HOLOSCAN_MODEL_PATH {models_folder}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Setup imports\n", "\n", "Let's import necessary classes/decorators and define `MEDNIST_CLASSES`." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "import logging\n", "import os\n", "from pathlib import Path\n", "from typing import Optional\n", "\n", "import torch\n", "\n", "from monai.deploy.conditions import CountCondition\n", "from monai.deploy.core import AppContext, Application, ConditionType, Fragment, Image, Operator, OperatorSpec\n", "from monai.deploy.operators.dicom_text_sr_writer_operator import DICOMTextSRWriterOperator, EquipmentInfo, ModelInfo\n", "from monai.transforms import EnsureChannelFirst, Compose, EnsureType, ScaleIntensity\n", "\n", "MEDNIST_CLASSES = [\"AbdomenCT\", \"BreastMRI\", \"CXR\", \"ChestCT\", \"Hand\", \"HeadCT\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Creating Operator classes\n", "\n", "##### LoadPILOperator" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "class LoadPILOperator(Operator):\n", " \"\"\"Load image from the given input (DataPath) and set numpy array to the output (Image).\"\"\"\n", "\n", " DEFAULT_INPUT_FOLDER = Path.cwd() / \"input\"\n", " DEFAULT_OUTPUT_NAME = \"image\"\n", "\n", " # For now, need to have the input folder as an instance attribute, set on init.\n", " # If dynamically changing the input folder, per compute, then use a (optional) input port to convey the\n", " # value of the input folder, which is then emitted by a upstream operator.\n", " def __init__(\n", " self,\n", " fragment: Fragment,\n", " *args,\n", " input_folder: Path = DEFAULT_INPUT_FOLDER,\n", " output_name: str = DEFAULT_OUTPUT_NAME,\n", " **kwargs,\n", " ):\n", " \"\"\"Creates an loader object with the input folder and the output port name overrides as needed.\n", "\n", " Args:\n", " fragment (Fragment): An instance of the Application class which is derived from Fragment.\n", " input_folder (Path): Folder from which to load input file(s).\n", " Defaults to `input` in the current working directory.\n", " output_name (str): Name of the output port, which is an image object. Defaults to `image`.\n", " \"\"\"\n", "\n", " self._logger = logging.getLogger(\"{}.{}\".format(__name__, type(self).__name__))\n", " self.input_path = input_folder\n", " self.index = 0\n", " self.output_name_image = (\n", " output_name.strip() if output_name and len(output_name.strip()) > 0 else LoadPILOperator.DEFAULT_OUTPUT_NAME\n", " )\n", "\n", " super().__init__(fragment, *args, **kwargs)\n", "\n", " def setup(self, spec: OperatorSpec):\n", " \"\"\"Set up the named input and output port(s)\"\"\"\n", " spec.output(self.output_name_image)\n", "\n", " def compute(self, op_input, op_output, context):\n", " import numpy as np\n", " from PIL import Image as PILImage\n", "\n", " # Input path is stored in the object attribute, but could change to use a named port if need be.\n", " input_path = self.input_path\n", " if input_path.is_dir():\n", " input_path = next(self.input_path.glob(\"*.*\")) # take the first file\n", "\n", " image = PILImage.open(input_path)\n", " image = image.convert(\"L\") # convert to greyscale image\n", " image_arr = np.asarray(image)\n", "\n", " output_image = Image(image_arr) # create Image domain object with a numpy array\n", " op_output.emit(output_image, self.output_name_image) # cannot omit the name even if single output.\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### MedNISTClassifierOperator" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "class MedNISTClassifierOperator(Operator):\n", " \"\"\"Classifies the given image and returns the class name.\n", "\n", " Named inputs:\n", " image: Image object for which to generate the classification.\n", " output_folder: Optional, the path to save the results JSON file, overridingthe the one set on __init__\n", "\n", " Named output:\n", " result_text: The classification results in text.\n", " \"\"\"\n", "\n", " DEFAULT_OUTPUT_FOLDER = Path.cwd() / \"classification_results\"\n", " # For testing the app directly, the model should be at the following path.\n", " MODEL_LOCAL_PATH = Path(os.environ.get(\"HOLOSCAN_MODEL_PATH\", Path.cwd() / \"model/model.ts\"))\n", "\n", " def __init__(\n", " self,\n", " fragment: Fragment,\n", " *args,\n", " app_context: AppContext,\n", " model_name: Optional[str] = \"\",\n", " model_path: Path = MODEL_LOCAL_PATH,\n", " output_folder: Path = DEFAULT_OUTPUT_FOLDER,\n", " **kwargs,\n", " ):\n", " \"\"\"Creates an instance with the reference back to the containing application/fragment.\n", "\n", " fragment (Fragment): An instance of the Application class which is derived from Fragment.\n", " model_name (str, optional): Name of the model. Default to \"\" for single model app.\n", " model_path (Path): Path to the model file. Defaults to model/models.ts of current working dir.\n", " output_folder (Path, optional): output folder for saving the classification results JSON file.\n", " \"\"\"\n", "\n", " # the names used for the model inference input and output\n", " self._input_dataset_key = \"image\"\n", " self._pred_dataset_key = \"pred\"\n", "\n", " # The names used for the operator input and output\n", " self.input_name_image = \"image\"\n", " self.output_name_result = \"result_text\"\n", "\n", " # The name of the optional input port for passing data to override the output folder path.\n", " self.input_name_output_folder = \"output_folder\"\n", "\n", " # The output folder set on the object can be overridden at each compute by data in the optional named input\n", " self.output_folder = output_folder\n", "\n", " # Need the name when there are multiple models loaded\n", " self._model_name = model_name.strip() if isinstance(model_name, str) else \"\"\n", " # Need the path to load the models when they are not loaded in the execution context\n", " self.model_path = model_path\n", " self.app_context = app_context\n", " self.model = self._get_model(self.app_context, self.model_path, self._model_name)\n", "\n", " # This needs to be at the end of the constructor.\n", " super().__init__(fragment, *args, **kwargs)\n", "\n", " def _get_model(self, app_context: AppContext, model_path: Path, model_name: str):\n", " \"\"\"Load the model with the given name from context or model path\n", "\n", " Args:\n", " app_context (AppContext): The application context object holding the model(s)\n", " model_path (Path): The path to the model file, as a backup to load model directly\n", " model_name (str): The name of the model, when multiples are loaded in the context\n", " \"\"\"\n", "\n", " if app_context.models:\n", " # `app_context.models.get(model_name)` returns a model instance if exists.\n", " # If model_name is not specified and only one model exists, it returns that model.\n", " model = app_context.models.get(model_name)\n", " else:\n", " model = torch.jit.load(\n", " MedNISTClassifierOperator.MODEL_LOCAL_PATH,\n", " map_location=torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),\n", " )\n", "\n", " return model\n", "\n", " def setup(self, spec: OperatorSpec):\n", " \"\"\"Set up the operator named input and named output, both are in-memory objects.\"\"\"\n", "\n", " spec.input(self.input_name_image)\n", " spec.input(self.input_name_output_folder).condition(ConditionType.NONE) # Optional for overriding.\n", " spec.output(self.output_name_result).condition(ConditionType.NONE) # Not forcing a downstream receiver.\n", "\n", " @property\n", " def transform(self):\n", " return Compose([EnsureChannelFirst(channel_dim=\"no_channel\"), ScaleIntensity(), EnsureType()])\n", "\n", " def compute(self, op_input, op_output, context):\n", " import json\n", "\n", " import torch\n", "\n", " img = op_input.receive(self.input_name_image).asnumpy() # (64, 64), uint8. Input validation can be added.\n", " image_tensor = self.transform(img) # (1, 64, 64), torch.float64\n", " image_tensor = image_tensor[None].float() # (1, 1, 64, 64), torch.float32\n", "\n", " device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", " image_tensor = image_tensor.to(device)\n", "\n", " with torch.no_grad():\n", " outputs = self.model(image_tensor)\n", "\n", " _, output_classes = outputs.max(dim=1)\n", "\n", " result = MEDNIST_CLASSES[output_classes[0]] # get the class name\n", " print(result)\n", " op_output.emit(result, self.output_name_result)\n", "\n", " # Get output folder, with value in optional input port overriding the obj attribute\n", " output_folder_on_compute = op_input.receive(self.input_name_output_folder) or self.output_folder\n", " Path.mkdir(output_folder_on_compute, parents=True, exist_ok=True) # Let exception bubble up if raised.\n", " output_path = output_folder_on_compute / \"output.json\"\n", " with open(output_path, \"w\") as fp:\n", " json.dump(result, fp)\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Creating Application class\n", "\n", "Our application class would look like below.\n", "\n", "It defines `App` class inheriting `Application` class.\n", "\n", "`LoadPILOperator` is connected to `MedNISTClassifierOperator` by using `self.add_flow()` in `compose()` method of `App`." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "class App(Application):\n", " \"\"\"Application class for the MedNIST classifier.\"\"\"\n", "\n", " def compose(self):\n", " app_context = Application.init_app_context({}) # Do not pass argv in Jupyter Notebook\n", " app_input_path = Path(app_context.input_path)\n", " app_output_path = Path(app_context.output_path)\n", " model_path = Path(app_context.model_path)\n", " load_pil_op = LoadPILOperator(self, CountCondition(self, 1), input_folder=app_input_path, name=\"pil_loader_op\")\n", " classifier_op = MedNISTClassifierOperator(\n", " self, app_context=app_context, output_folder=app_output_path, model_path=model_path, name=\"classifier_op\"\n", " )\n", "\n", " my_model_info = ModelInfo(\"MONAI WG Trainer\", \"MEDNIST Classifier\", \"0.1\", \"xyz\")\n", " my_equipment = EquipmentInfo(manufacturer=\"MOANI Deploy App SDK\", manufacturer_model=\"DICOM SR Writer\")\n", " my_special_tags = {\"SeriesDescription\": \"Not for clinical use. The result is for research use only.\"}\n", " dicom_sr_operator = DICOMTextSRWriterOperator(\n", " self,\n", " copy_tags=False,\n", " model_info=my_model_info,\n", " equipment_info=my_equipment,\n", " custom_tags=my_special_tags,\n", " output_folder=app_output_path,\n", " )\n", "\n", " self.add_flow(load_pil_op, classifier_op, {(\"image\", \"image\")})\n", " self.add_flow(classifier_op, dicom_sr_operator, {(\"result_text\", \"text\")})\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Executing app locally\n", "\n", "The test input file file, output path, and model have been prepared, and the paths set in the environment variables, so we can go ahead and execute the application Jupyter notebook with a clean output folder." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "[info] [fragment.cpp:599] Loading extensions from configs...\n", "[2025-01-29 14:14:52,365] [INFO] (root) - Parsed args: Namespace(log_level=None, input=None, output=None, model=None, workdir=None, argv=[])\n", "[2025-01-29 14:14:52,380] [INFO] (root) - AppContext object: AppContext(input_path=input, output_path=output, model_path=models, workdir=)\n", "[info] [gxf_executor.cpp:264] Creating context\n", "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'output_folder'\n", "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'image'\n", "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'study_selected_series_list'\n", "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'text'\n", "[info] [gxf_executor.cpp:2208] Activating Graph...\n", "[info] [gxf_executor.cpp:2238] Running Graph...\n", "[info] [gxf_executor.cpp:2240] Waiting for completion...\n", "[info] [greedy_scheduler.cpp:191] Scheduling 3 entities\n", "/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/monai/data/meta_tensor.py:116: UserWarning: The given NumPy array is not writable, and PyTorch does not support non-writable tensors. This means writing to this tensor will result in undefined behavior. You may want to copy the array to protect its data or make it writable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at ../torch/csrc/utils/tensor_numpy.cpp:206.)\n", " return torch.as_tensor(x, *args, **_kwargs).as_subclass(cls)\n", "[2025-01-29 14:14:53,399] [WARNING] (pydicom) - 'Dataset.is_implicit_VR' will be removed in v4.0, set the Transfer Syntax UID or use the 'implicit_vr' argument with Dataset.save_as() or dcmwrite() instead\n", "[2025-01-29 14:14:53,400] [WARNING] (pydicom) - 'Dataset.is_little_endian' will be removed in v4.0, set the Transfer Syntax UID or use the 'little_endian' argument with Dataset.save_as() or dcmwrite() instead\n", "[2025-01-29 14:14:53,402] [WARNING] (pydicom) - Invalid value for VR UI: 'xyz'. Please see for allowed values for each VR.\n", "/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/pydicom/valuerep.py:440: UserWarning: Invalid value for VR UI: 'xyz'. Please see for allowed values for each VR.\n", " warn_and_log(msg)\n", "[2025-01-29 14:14:53,406] [WARNING] (pydicom) - 'write_like_original' is deprecated and will be removed in v4.0, please use 'enforce_file_format' instead\n", "[2025-01-29 14:14:53,410] [INFO] (root) - Finished writing DICOM instance to file output/1.2.826.0.1.3680043.8.498.89440030592013337302433440951243230255.dcm\n", "[2025-01-29 14:14:53,413] [INFO] (monai.deploy.operators.dicom_text_sr_writer_operator.DICOMTextSRWriterOperator) - DICOM SOP instance saved in output/1.2.826.0.1.3680043.8.498.89440030592013337302433440951243230255.dcm\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "AbdomenCT\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "[info] [greedy_scheduler.cpp:372] Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.\n", "[info] [greedy_scheduler.cpp:401] Scheduler finished.\n", "[info] [gxf_executor.cpp:2243] Deactivating Graph...\n", "[info] [gxf_executor.cpp:2251] Graph execution finished.\n", "[info] [gxf_executor.cpp:294] Destroying context\n" ] } ], "source": [ "!rm -rf $HOLOSCAN_OUTPUT_PATH\n", "app = App().run()" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\"AbdomenCT\"" ] } ], "source": [ "!cat $HOLOSCAN_OUTPUT_PATH/output.json" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once the application is verified inside Jupyter notebook, we can write the whole application as a file(`mednist_classifier_monaideploy.py`) by concatenating code above, then add the following lines:\n", "\n", "```python\n", "if __name__ == \"__main__\":\n", " App()\n", "```\n", "\n", "The above lines are needed to execute the application code by using `python` interpreter." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "# Create an application folder\n", "!mkdir -p mednist_app\n", "!rm -rf mednist_app/*" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Writing mednist_app/mednist_classifier_monaideploy.py\n" ] } ], "source": [ "%%writefile mednist_app/mednist_classifier_monaideploy.py\n", "\n", "# Copyright 2021-2023 MONAI Consortium\n", "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", "# you may not use this file except in compliance with the License.\n", "# You may obtain a copy of the License at\n", "# http://www.apache.org/licenses/LICENSE-2.0\n", "# Unless required by applicable law or agreed to in writing, software\n", "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", "# See the License for the specific language governing permissions and\n", "# limitations under the License.\n", "\n", "import logging\n", "import os\n", "from pathlib import Path\n", "from typing import Optional\n", "\n", "import torch\n", "\n", "from monai.deploy.conditions import CountCondition\n", "from monai.deploy.core import AppContext, Application, ConditionType, Fragment, Image, Operator, OperatorSpec\n", "from monai.deploy.operators.dicom_text_sr_writer_operator import DICOMTextSRWriterOperator, EquipmentInfo, ModelInfo\n", "from monai.transforms import EnsureChannelFirst, Compose, EnsureType, ScaleIntensity\n", "\n", "MEDNIST_CLASSES = [\"AbdomenCT\", \"BreastMRI\", \"CXR\", \"ChestCT\", \"Hand\", \"HeadCT\"]\n", "\n", "\n", "# @md.env(pip_packages=[\"pillow\"])\n", "class LoadPILOperator(Operator):\n", " \"\"\"Load image from the given input (DataPath) and set numpy array to the output (Image).\"\"\"\n", "\n", " DEFAULT_INPUT_FOLDER = Path.cwd() / \"input\"\n", " DEFAULT_OUTPUT_NAME = \"image\"\n", "\n", " # For now, need to have the input folder as an instance attribute, set on init.\n", " # If dynamically changing the input folder, per compute, then use a (optional) input port to convey the\n", " # value of the input folder, which is then emitted by a upstream operator.\n", " def __init__(\n", " self,\n", " fragment: Fragment,\n", " *args,\n", " input_folder: Path = DEFAULT_INPUT_FOLDER,\n", " output_name: str = DEFAULT_OUTPUT_NAME,\n", " **kwargs,\n", " ):\n", " \"\"\"Creates an loader object with the input folder and the output port name overrides as needed.\n", "\n", " Args:\n", " fragment (Fragment): An instance of the Application class which is derived from Fragment.\n", " input_folder (Path): Folder from which to load input file(s).\n", " Defaults to `input` in the current working directory.\n", " output_name (str): Name of the output port, which is an image object. Defaults to `image`.\n", " \"\"\"\n", "\n", " self._logger = logging.getLogger(\"{}.{}\".format(__name__, type(self).__name__))\n", " self.input_path = input_folder\n", " self.index = 0\n", " self.output_name_image = (\n", " output_name.strip() if output_name and len(output_name.strip()) > 0 else LoadPILOperator.DEFAULT_OUTPUT_NAME\n", " )\n", "\n", " super().__init__(fragment, *args, **kwargs)\n", "\n", " def setup(self, spec: OperatorSpec):\n", " \"\"\"Set up the named input and output port(s)\"\"\"\n", " spec.output(self.output_name_image)\n", "\n", " def compute(self, op_input, op_output, context):\n", " import numpy as np\n", " from PIL import Image as PILImage\n", "\n", " # Input path is stored in the object attribute, but could change to use a named port if need be.\n", " input_path = self.input_path\n", " if input_path.is_dir():\n", " input_path = next(self.input_path.glob(\"*.*\")) # take the first file\n", "\n", " image = PILImage.open(input_path)\n", " image = image.convert(\"L\") # convert to greyscale image\n", " image_arr = np.asarray(image)\n", "\n", " output_image = Image(image_arr) # create Image domain object with a numpy array\n", " op_output.emit(output_image, self.output_name_image) # cannot omit the name even if single output.\n", "\n", "\n", "# @md.env(pip_packages=[\"monai\"])\n", "class MedNISTClassifierOperator(Operator):\n", " \"\"\"Classifies the given image and returns the class name.\n", "\n", " Named inputs:\n", " image: Image object for which to generate the classification.\n", " output_folder: Optional, the path to save the results JSON file, overridingthe the one set on __init__\n", "\n", " Named output:\n", " result_text: The classification results in text.\n", " \"\"\"\n", "\n", " DEFAULT_OUTPUT_FOLDER = Path.cwd() / \"classification_results\"\n", " # For testing the app directly, the model should be at the following path.\n", " MODEL_LOCAL_PATH = Path(os.environ.get(\"HOLOSCAN_MODEL_PATH\", Path.cwd() / \"model/model.ts\"))\n", "\n", " def __init__(\n", " self,\n", " fragment: Fragment,\n", " *args,\n", " app_context: AppContext,\n", " model_name: Optional[str] = \"\",\n", " model_path: Path = MODEL_LOCAL_PATH,\n", " output_folder: Path = DEFAULT_OUTPUT_FOLDER,\n", " **kwargs,\n", " ):\n", " \"\"\"Creates an instance with the reference back to the containing application/fragment.\n", "\n", " fragment (Fragment): An instance of the Application class which is derived from Fragment.\n", " model_name (str, optional): Name of the model. Default to \"\" for single model app.\n", " model_path (Path): Path to the model file. Defaults to model/models.ts of current working dir.\n", " output_folder (Path, optional): output folder for saving the classification results JSON file.\n", " \"\"\"\n", "\n", " # the names used for the model inference input and output\n", " self._input_dataset_key = \"image\"\n", " self._pred_dataset_key = \"pred\"\n", "\n", " # The names used for the operator input and output\n", " self.input_name_image = \"image\"\n", " self.output_name_result = \"result_text\"\n", "\n", " # The name of the optional input port for passing data to override the output folder path.\n", " self.input_name_output_folder = \"output_folder\"\n", "\n", " # The output folder set on the object can be overridden at each compute by data in the optional named input\n", " self.output_folder = output_folder\n", "\n", " # Need the name when there are multiple models loaded\n", " self._model_name = model_name.strip() if isinstance(model_name, str) else \"\"\n", " # Need the path to load the models when they are not loaded in the execution context\n", " self.model_path = model_path\n", " self.app_context = app_context\n", " self.model = self._get_model(self.app_context, self.model_path, self._model_name)\n", "\n", " # This needs to be at the end of the constructor.\n", " super().__init__(fragment, *args, **kwargs)\n", "\n", " def _get_model(self, app_context: AppContext, model_path: Path, model_name: str):\n", " \"\"\"Load the model with the given name from context or model path\n", "\n", " Args:\n", " app_context (AppContext): The application context object holding the model(s)\n", " model_path (Path): The path to the model file, as a backup to load model directly\n", " model_name (str): The name of the model, when multiples are loaded in the context\n", " \"\"\"\n", "\n", " if app_context.models:\n", " # `app_context.models.get(model_name)` returns a model instance if exists.\n", " # If model_name is not specified and only one model exists, it returns that model.\n", " model = app_context.models.get(model_name)\n", " else:\n", " model = torch.jit.load(\n", " MedNISTClassifierOperator.MODEL_LOCAL_PATH,\n", " map_location=torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),\n", " )\n", "\n", " return model\n", "\n", " def setup(self, spec: OperatorSpec):\n", " \"\"\"Set up the operator named input and named output, both are in-memory objects.\"\"\"\n", "\n", " spec.input(self.input_name_image)\n", " spec.input(self.input_name_output_folder).condition(ConditionType.NONE) # Optional for overriding.\n", " spec.output(self.output_name_result).condition(ConditionType.NONE) # Not forcing a downstream receiver.\n", "\n", " @property\n", " def transform(self):\n", " return Compose([EnsureChannelFirst(channel_dim=\"no_channel\"), ScaleIntensity(), EnsureType()])\n", "\n", " def compute(self, op_input, op_output, context):\n", " import json\n", "\n", " import torch\n", "\n", " img = op_input.receive(self.input_name_image).asnumpy() # (64, 64), uint8. Input validation can be added.\n", " image_tensor = self.transform(img) # (1, 64, 64), torch.float64\n", " image_tensor = image_tensor[None].float() # (1, 1, 64, 64), torch.float32\n", "\n", " device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", " image_tensor = image_tensor.to(device)\n", "\n", " with torch.no_grad():\n", " outputs = self.model(image_tensor)\n", "\n", " _, output_classes = outputs.max(dim=1)\n", "\n", " result = MEDNIST_CLASSES[output_classes[0]] # get the class name\n", " print(result)\n", " op_output.emit(result, self.output_name_result)\n", "\n", " # Get output folder, with value in optional input port overriding the obj attribute\n", " output_folder_on_compute = op_input.receive(self.input_name_output_folder) or self.output_folder\n", " Path.mkdir(output_folder_on_compute, parents=True, exist_ok=True) # Let exception bubble up if raised.\n", " output_path = output_folder_on_compute / \"output.json\"\n", " with open(output_path, \"w\") as fp:\n", " json.dump(result, fp)\n", "\n", "\n", "# @md.resource(cpu=1, gpu=1, memory=\"1Gi\")\n", "class App(Application):\n", " \"\"\"Application class for the MedNIST classifier.\"\"\"\n", "\n", " def compose(self):\n", " app_context = AppContext({}) # Let it figure out all the attributes without overriding\n", " app_input_path = Path(app_context.input_path)\n", " app_output_path = Path(app_context.output_path)\n", " model_path = Path(app_context.model_path)\n", " load_pil_op = LoadPILOperator(self, CountCondition(self, 1), input_folder=app_input_path, name=\"pil_loader_op\")\n", " classifier_op = MedNISTClassifierOperator(\n", " self, app_context=app_context, output_folder=app_output_path, model_path=model_path, name=\"classifier_op\"\n", " )\n", "\n", " my_model_info = ModelInfo(\"MONAI WG Trainer\", \"MEDNIST Classifier\", \"0.1\", \"xyz\")\n", " my_equipment = EquipmentInfo(manufacturer=\"MOANI Deploy App SDK\", manufacturer_model=\"DICOM SR Writer\")\n", " my_special_tags = {\"SeriesDescription\": \"Not for clinical use. The result is for research use only.\"}\n", " dicom_sr_operator = DICOMTextSRWriterOperator(\n", " self,\n", " copy_tags=False,\n", " model_info=my_model_info,\n", " equipment_info=my_equipment,\n", " custom_tags=my_special_tags,\n", " output_folder=app_output_path,\n", " )\n", "\n", " self.add_flow(load_pil_op, classifier_op, {(\"image\", \"image\")})\n", " self.add_flow(classifier_op, dicom_sr_operator, {(\"result_text\", \"text\")})\n", "\n", "\n", "if __name__ == \"__main__\":\n", " App().run()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This time, let's execute the app in the command line." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[\u001b[32minfo\u001b[m] [fragment.cpp:599] Loading extensions from configs...\n", "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:264] Creating context\n", "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'output_folder'\n", "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'image'\n", "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'study_selected_series_list'\n", "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:1797] creating input IOSpec named 'text'\n", "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2208] Activating Graph...\n", "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2238] Running Graph...\n", "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2240] Waiting for completion...\n", "[\u001b[32minfo\u001b[m] [greedy_scheduler.cpp:191] Scheduling 3 entities\n", "/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/monai/data/meta_tensor.py:116: UserWarning: The given NumPy array is not writable, and PyTorch does not support non-writable tensors. This means writing to this tensor will result in undefined behavior. You may want to copy the array to protect its data or make it writable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at ../torch/csrc/utils/tensor_numpy.cpp:206.)\n", " return torch.as_tensor(x, *args, **_kwargs).as_subclass(cls)\n", "AbdomenCT\n", "WARNING:pydicom:'Dataset.is_implicit_VR' will be removed in v4.0, set the Transfer Syntax UID or use the 'implicit_vr' argument with Dataset.save_as() or dcmwrite() instead\n", "WARNING:pydicom:'Dataset.is_little_endian' will be removed in v4.0, set the Transfer Syntax UID or use the 'little_endian' argument with Dataset.save_as() or dcmwrite() instead\n", "WARNING:pydicom:Invalid value for VR UI: 'xyz'. Please see for allowed values for each VR.\n", "/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.10/site-packages/pydicom/valuerep.py:440: UserWarning: Invalid value for VR UI: 'xyz'. Please see for allowed values for each VR.\n", " warn_and_log(msg)\n", "WARNING:pydicom:'write_like_original' is deprecated and will be removed in v4.0, please use 'enforce_file_format' instead\n", "[\u001b[32minfo\u001b[m] [greedy_scheduler.cpp:372] Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.\n", "[\u001b[32minfo\u001b[m] [greedy_scheduler.cpp:401] Scheduler finished.\n", "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2243] Deactivating Graph...\n", "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:2251] Graph execution finished.\n", "[\u001b[32minfo\u001b[m] [gxf_executor.cpp:294] Destroying context\n" ] } ], "source": [ "!python \"mednist_app/mednist_classifier_monaideploy.py\"" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\"AbdomenCT\"" ] } ], "source": [ "!cat $HOLOSCAN_OUTPUT_PATH/output.json" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Packaging app" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's package the app with MONAI Application Packager.\n", "\n", "In this version of the App SDK, we need to write out the configuration yaml file as well as the package requirements file, in the application folder." ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Writing mednist_app/app.yaml\n" ] } ], "source": [ "%%writefile mednist_app/app.yaml\n", "%YAML 1.2\n", "---\n", "application:\n", " title: MONAI Deploy App Package - MedNIST Classifier App\n", " version: 1.0\n", " inputFormats: [\"file\"]\n", " outputFormats: [\"file\"]\n", "\n", "resources:\n", " cpu: 1\n", " gpu: 1\n", " memory: 1Gi\n", " gpuMemory: 1Gi" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Writing mednist_app/requirements.txt\n" ] } ], "source": [ "%%writefile mednist_app/requirements.txt\n", "monai>=1.2.0\n", "Pillow>=8.4.0\n", "pydicom>=2.3.0\n", "highdicom>=0.18.2\n", "SimpleITK>=2.0.0\n", "setuptools>=59.5.0 # for pkg_resources\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[2025-01-29 14:15:03,458] [INFO] (common) - Downloading CLI manifest file...\n", "[2025-01-29 14:15:03,859] [DEBUG] (common) - Validating CLI manifest file...\n", "[2025-01-29 14:15:03,859] [INFO] (packager.parameters) - Application: /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/mednist_app/mednist_classifier_monaideploy.py\n", "[2025-01-29 14:15:03,859] [INFO] (packager.parameters) - Detected application type: Python File\n", "[2025-01-29 14:15:03,860] [INFO] (packager) - Scanning for models in /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/models...\n", "[2025-01-29 14:15:03,860] [DEBUG] (packager) - Model model=/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/models/model added.\n", "[2025-01-29 14:15:03,860] [INFO] (packager) - Reading application configuration from /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/mednist_app/app.yaml...\n", "[2025-01-29 14:15:03,864] [INFO] (packager) - Generating app.json...\n", "[2025-01-29 14:15:03,864] [INFO] (packager) - Generating pkg.json...\n", "[2025-01-29 14:15:03,869] [DEBUG] (common) - \n", "=============== Begin app.json ===============\n", "{\n", " \"apiVersion\": \"1.0.0\",\n", " \"command\": \"[\\\"python3\\\", \\\"/opt/holoscan/app/mednist_classifier_monaideploy.py\\\"]\",\n", " \"environment\": {\n", " \"HOLOSCAN_APPLICATION\": \"/opt/holoscan/app\",\n", " \"HOLOSCAN_INPUT_PATH\": \"input/\",\n", " \"HOLOSCAN_OUTPUT_PATH\": \"output/\",\n", " \"HOLOSCAN_WORKDIR\": \"/var/holoscan\",\n", " \"HOLOSCAN_MODEL_PATH\": \"/opt/holoscan/models\",\n", " \"HOLOSCAN_CONFIG_PATH\": \"/var/holoscan/app.yaml\",\n", " \"HOLOSCAN_APP_MANIFEST_PATH\": \"/etc/holoscan/app.json\",\n", " \"HOLOSCAN_PKG_MANIFEST_PATH\": \"/etc/holoscan/pkg.json\",\n", " \"HOLOSCAN_DOCS_PATH\": \"/opt/holoscan/docs\",\n", " \"HOLOSCAN_LOGS_PATH\": \"/var/holoscan/logs\"\n", " },\n", " \"input\": {\n", " \"path\": \"input/\",\n", " \"formats\": null\n", " },\n", " \"liveness\": null,\n", " \"output\": {\n", " \"path\": \"output/\",\n", " \"formats\": null\n", " },\n", " \"readiness\": null,\n", " \"sdk\": \"monai-deploy\",\n", " \"sdkVersion\": \"2.0.0\",\n", " \"timeout\": 0,\n", " \"version\": 1.0,\n", " \"workingDirectory\": \"/var/holoscan\"\n", "}\n", "================ End app.json ================\n", " \n", "[2025-01-29 14:15:03,869] [DEBUG] (common) - \n", "=============== Begin pkg.json ===============\n", "{\n", " \"apiVersion\": \"1.0.0\",\n", " \"applicationRoot\": \"/opt/holoscan/app\",\n", " \"modelRoot\": \"/opt/holoscan/models\",\n", " \"models\": {\n", " \"model\": \"/opt/holoscan/models/model\"\n", " },\n", " \"resources\": {\n", " \"cpu\": 1,\n", " \"gpu\": 1,\n", " \"memory\": \"1Gi\",\n", " \"gpuMemory\": \"1Gi\"\n", " },\n", " \"version\": 1.0,\n", " \"platformConfig\": \"dgpu\"\n", "}\n", "================ End pkg.json ================\n", " \n", "[2025-01-29 14:15:03,900] [DEBUG] (packager.builder) - \n", "========== Begin Build Parameters ==========\n", "{'additional_lib_paths': '',\n", " 'app_config_file_path': PosixPath('/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/mednist_app/app.yaml'),\n", " 'app_dir': PosixPath('/opt/holoscan/app'),\n", " 'app_json': '/etc/holoscan/app.json',\n", " 'application': PosixPath('/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/mednist_app/mednist_classifier_monaideploy.py'),\n", " 'application_directory': PosixPath('/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/mednist_app'),\n", " 'application_type': 'PythonFile',\n", " 'build_cache': PosixPath('/home/mqin/.holoscan_build_cache'),\n", " 'cmake_args': '',\n", " 'command': '[\"python3\", '\n", " '\"/opt/holoscan/app/mednist_classifier_monaideploy.py\"]',\n", " 'command_filename': 'mednist_classifier_monaideploy.py',\n", " 'config_file_path': PosixPath('/var/holoscan/app.yaml'),\n", " 'docs_dir': PosixPath('/opt/holoscan/docs'),\n", " 'full_input_path': PosixPath('/var/holoscan/input'),\n", " 'full_output_path': PosixPath('/var/holoscan/output'),\n", " 'gid': 1000,\n", " 'holoscan_sdk_version': '2.9.0',\n", " 'includes': [],\n", " 'input_dir': 'input/',\n", " 'lib_dir': PosixPath('/opt/holoscan/lib'),\n", " 'logs_dir': PosixPath('/var/holoscan/logs'),\n", " 'models': {'model': PosixPath('/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/models/model')},\n", " 'models_dir': PosixPath('/opt/holoscan/models'),\n", " 'monai_deploy_app_sdk_version': '2.0.0',\n", " 'no_cache': False,\n", " 'output_dir': 'output/',\n", " 'pip_packages': None,\n", " 'pkg_json': '/etc/holoscan/pkg.json',\n", " 'requirements_file_path': PosixPath('/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/mednist_app/requirements.txt'),\n", " 'sdk': ,\n", " 'sdk_type': 'monai-deploy',\n", " 'tarball_output': None,\n", " 'timeout': 0,\n", " 'title': 'MONAI Deploy App Package - MedNIST Classifier App',\n", " 'uid': 1000,\n", " 'username': 'holoscan',\n", " 'version': 1.0,\n", " 'working_dir': PosixPath('/var/holoscan')}\n", "=========== End Build Parameters ===========\n", "\n", "[2025-01-29 14:15:03,900] [DEBUG] (packager.builder) - \n", "========== Begin Platform Parameters ==========\n", "{'base_image': 'nvcr.io/nvidia/cuda:12.6.0-runtime-ubuntu22.04',\n", " 'build_image': None,\n", " 'cuda_deb_arch': 'x86_64',\n", " 'custom_base_image': False,\n", " 'custom_holoscan_sdk': False,\n", " 'custom_monai_deploy_sdk': False,\n", " 'gpu_type': 'dgpu',\n", " 'holoscan_deb_arch': 'amd64',\n", " 'holoscan_sdk_file': '2.9.0',\n", " 'holoscan_sdk_filename': '2.9.0',\n", " 'monai_deploy_sdk_file': None,\n", " 'monai_deploy_sdk_filename': None,\n", " 'tag': 'mednist_app:1.0',\n", " 'target_arch': 'x86_64'}\n", "=========== End Platform Parameters ===========\n", "\n", "[2025-01-29 14:15:03,917] [DEBUG] (packager.builder) - \n", "========== Begin Dockerfile ==========\n", "\n", "ARG GPU_TYPE=dgpu\n", "\n", "\n", "\n", "\n", "FROM nvcr.io/nvidia/cuda:12.6.0-runtime-ubuntu22.04 AS base\n", "\n", "RUN apt-get update \\\n", " && apt-get install -y --no-install-recommends --no-install-suggests \\\n", " curl \\\n", " jq \\\n", " && rm -rf /var/lib/apt/lists/*\n", "\n", "\n", "\n", "\n", "# FROM base AS mofed-installer\n", "# ARG MOFED_VERSION=23.10-2.1.3.1\n", "\n", "# # In a container, we only need to install the user space libraries, though the drivers are still\n", "# # needed on the host.\n", "# # Note: MOFED's installation is not easily portable, so we can't copy the output of this stage\n", "# # to our final stage, but must inherit from it. For that reason, we keep track of the build/install\n", "# # only dependencies in the `MOFED_DEPS` variable (parsing the output of `--check-deps-only`) to\n", "# # remove them in that same layer, to ensure they are not propagated in the final image.\n", "# WORKDIR /opt/nvidia/mofed\n", "# ARG MOFED_INSTALL_FLAGS=\"--dpdk --with-mft --user-space-only --force --without-fw-update\"\n", "# RUN UBUNTU_VERSION=$(cat /etc/lsb-release | grep DISTRIB_RELEASE | cut -d= -f2) \\\n", "# && OFED_PACKAGE=\"MLNX_OFED_LINUX-${MOFED_VERSION}-ubuntu${UBUNTU_VERSION}-$(uname -m)\" \\\n", "# && curl -S -# -o ${OFED_PACKAGE}.tgz -L \\\n", "# https://www.mellanox.com/downloads/ofed/MLNX_OFED-${MOFED_VERSION}/${OFED_PACKAGE}.tgz \\\n", "# && tar xf ${OFED_PACKAGE}.tgz \\\n", "# && MOFED_INSTALLER=$(find . -name mlnxofedinstall -type f -executable -print) \\\n", "# && MOFED_DEPS=$(${MOFED_INSTALLER} ${MOFED_INSTALL_FLAGS} --check-deps-only 2>/dev/null | tail -n1 | cut -d' ' -f3-) \\\n", "# && apt-get update \\\n", "# && apt-get install --no-install-recommends -y ${MOFED_DEPS} \\\n", "# && ${MOFED_INSTALLER} ${MOFED_INSTALL_FLAGS} \\\n", "# && rm -r * \\\n", "# && apt-get remove -y ${MOFED_DEPS} && apt-get autoremove -y \\\n", "# && rm -rf /var/lib/apt/lists/*\n", "\n", "FROM base AS release\n", "ENV DEBIAN_FRONTEND=noninteractive\n", "ENV TERM=xterm-256color\n", "\n", "ARG GPU_TYPE\n", "ARG UNAME\n", "ARG UID\n", "ARG GID\n", "\n", "RUN mkdir -p /etc/holoscan/ \\\n", " && mkdir -p /opt/holoscan/ \\\n", " && mkdir -p /var/holoscan \\\n", " && mkdir -p /opt/holoscan/app \\\n", " && mkdir -p /var/holoscan/input \\\n", " && mkdir -p /var/holoscan/output\n", "\n", "LABEL base=\"nvcr.io/nvidia/cuda:12.6.0-runtime-ubuntu22.04\"\n", "LABEL tag=\"mednist_app:1.0\"\n", "LABEL org.opencontainers.image.title=\"MONAI Deploy App Package - MedNIST Classifier App\"\n", "LABEL org.opencontainers.image.version=\"1.0\"\n", "LABEL org.nvidia.holoscan=\"2.9.0\"\n", "\n", "LABEL org.monai.deploy.app-sdk=\"2.0.0\"\n", "\n", "ENV HOLOSCAN_INPUT_PATH=/var/holoscan/input\n", "ENV HOLOSCAN_OUTPUT_PATH=/var/holoscan/output\n", "ENV HOLOSCAN_WORKDIR=/var/holoscan\n", "ENV HOLOSCAN_APPLICATION=/opt/holoscan/app\n", "ENV HOLOSCAN_TIMEOUT=0\n", "ENV HOLOSCAN_MODEL_PATH=/opt/holoscan/models\n", "ENV HOLOSCAN_DOCS_PATH=/opt/holoscan/docs\n", "ENV HOLOSCAN_CONFIG_PATH=/var/holoscan/app.yaml\n", "ENV HOLOSCAN_APP_MANIFEST_PATH=/etc/holoscan/app.json\n", "ENV HOLOSCAN_PKG_MANIFEST_PATH=/etc/holoscan/pkg.json\n", "ENV HOLOSCAN_LOGS_PATH=/var/holoscan/logs\n", "ENV HOLOSCAN_VERSION=2.9.0\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "# If torch is installed, we can skip installing Python\n", "ENV PYTHON_VERSION=3.10.6-1~22.04\n", "ENV PYTHON_PIP_VERSION=22.0.2+dfsg-*\n", "\n", "RUN apt update \\\n", " && apt-get install -y --no-install-recommends --no-install-suggests \\\n", " python3-minimal=${PYTHON_VERSION} \\\n", " libpython3-stdlib=${PYTHON_VERSION} \\\n", " python3=${PYTHON_VERSION} \\\n", " python3-venv=${PYTHON_VERSION} \\\n", " python3-pip=${PYTHON_PIP_VERSION} \\\n", " && rm -rf /var/lib/apt/lists/*\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "RUN groupadd -f -g $GID $UNAME\n", "RUN useradd -rm -d /home/$UNAME -s /bin/bash -g $GID -G sudo -u $UID $UNAME\n", "RUN chown -R holoscan /var/holoscan && \\\n", " chown -R holoscan /var/holoscan/input && \\\n", " chown -R holoscan /var/holoscan/output\n", "\n", "# Set the working directory\n", "WORKDIR /var/holoscan\n", "\n", "# Copy HAP/MAP tool script\n", "COPY ./tools /var/holoscan/tools\n", "RUN chmod +x /var/holoscan/tools\n", "\n", "# Set the working directory\n", "WORKDIR /var/holoscan\n", "\n", "USER $UNAME\n", "\n", "ENV PATH=/home/${UNAME}/.local/bin:/opt/nvidia/holoscan/bin:$PATH\n", "ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/${UNAME}/.local/lib/python3.10/site-packages/holoscan/lib\n", "\n", "COPY ./pip/requirements.txt /tmp/requirements.txt\n", "\n", "RUN pip install --upgrade pip\n", "RUN pip install --no-cache-dir --user -r /tmp/requirements.txt\n", "\n", "\n", "# Install MONAI Deploy App SDK\n", "\n", "# Install MONAI Deploy from PyPI org\n", "RUN pip install monai-deploy-app-sdk==2.0.0\n", "\n", "\n", "COPY ./models /opt/holoscan/models\n", "\n", "\n", "COPY ./map/app.json /etc/holoscan/app.json\n", "COPY ./app.config /var/holoscan/app.yaml\n", "COPY ./map/pkg.json /etc/holoscan/pkg.json\n", "\n", "COPY ./app /opt/holoscan/app\n", "\n", "\n", "ENTRYPOINT [\"/var/holoscan/tools\"]\n", "=========== End Dockerfile ===========\n", "\n", "[2025-01-29 14:15:03,917] [INFO] (packager.builder) - \n", "===============================================================================\n", "Building image for: x64-workstation\n", " Architecture: linux/amd64\n", " Base Image: nvcr.io/nvidia/cuda:12.6.0-runtime-ubuntu22.04\n", " Build Image: N/A\n", " Cache: Enabled\n", " Configuration: dgpu\n", " Holoscan SDK Package: 2.9.0\n", " MONAI Deploy App SDK Package: N/A\n", " gRPC Health Probe: N/A\n", " SDK Version: 2.9.0\n", " SDK: monai-deploy\n", " Tag: mednist_app-x64-workstation-dgpu-linux-amd64:1.0\n", " Included features/dependencies: N/A\n", " \n", "[2025-01-29 14:15:04,216] [INFO] (common) - Using existing Docker BuildKit builder `holoscan_app_builder`\n", "[2025-01-29 14:15:04,216] [DEBUG] (packager.builder) - Building Holoscan Application Package: tag=mednist_app-x64-workstation-dgpu-linux-amd64:1.0\n", "#0 building with \"holoscan_app_builder\" instance using docker-container driver\n", "\n", "#1 [internal] load build definition from Dockerfile\n", "#1 transferring dockerfile: 4.57kB done\n", "#1 DONE 0.1s\n", "\n", "#2 [internal] load metadata for nvcr.io/nvidia/cuda:12.6.0-runtime-ubuntu22.04\n", "#2 ...\n", "\n", "#3 [auth] nvidia/cuda:pull token for nvcr.io\n", "#3 DONE 0.0s\n", "\n", "#2 [internal] load metadata for nvcr.io/nvidia/cuda:12.6.0-runtime-ubuntu22.04\n", "#2 DONE 0.5s\n", "\n", "#4 [internal] load .dockerignore\n", "#4 transferring context: 1.80kB done\n", "#4 DONE 0.1s\n", "\n", "#5 importing cache manifest from nvcr.io/nvidia/cuda:12.6.0-runtime-ubuntu22.04\n", "#5 ...\n", "\n", "#6 [internal] load build context\n", "#6 DONE 0.0s\n", "\n", "#7 importing cache manifest from local:12634971125111610588\n", "#7 inferred cache manifest type: application/vnd.oci.image.index.v1+json done\n", "#7 DONE 0.0s\n", "\n", "#8 [base 1/2] FROM nvcr.io/nvidia/cuda:12.6.0-runtime-ubuntu22.04@sha256:22fc009e5cea0b8b91d94c99fdd419d2366810b5ea835e47b8343bc15800c186\n", "#8 resolve nvcr.io/nvidia/cuda:12.6.0-runtime-ubuntu22.04@sha256:22fc009e5cea0b8b91d94c99fdd419d2366810b5ea835e47b8343bc15800c186 0.0s done\n", "#8 DONE 0.0s\n", "\n", "#5 importing cache manifest from nvcr.io/nvidia/cuda:12.6.0-runtime-ubuntu22.04\n", "#5 inferred cache manifest type: application/vnd.docker.distribution.manifest.list.v2+json done\n", "#5 DONE 0.3s\n", "\n", "#6 [internal] load build context\n", "#6 transferring context: 28.62MB 0.2s done\n", "#6 DONE 0.6s\n", "\n", "#9 [release 5/18] RUN chown -R holoscan /var/holoscan && chown -R holoscan /var/holoscan/input && chown -R holoscan /var/holoscan/output\n", "#9 CACHED\n", "\n", "#10 [release 8/18] RUN chmod +x /var/holoscan/tools\n", "#10 CACHED\n", "\n", "#11 [release 4/18] RUN useradd -rm -d /home/holoscan -s /bin/bash -g 1000 -G sudo -u 1000 holoscan\n", "#11 CACHED\n", "\n", "#12 [release 1/18] RUN mkdir -p /etc/holoscan/ && mkdir -p /opt/holoscan/ && mkdir -p /var/holoscan && mkdir -p /opt/holoscan/app && mkdir -p /var/holoscan/input && mkdir -p /var/holoscan/output\n", "#12 CACHED\n", "\n", "#13 [release 7/18] COPY ./tools /var/holoscan/tools\n", "#13 CACHED\n", "\n", "#14 [base 2/2] RUN apt-get update && apt-get install -y --no-install-recommends --no-install-suggests curl jq && rm -rf /var/lib/apt/lists/*\n", "#14 CACHED\n", "\n", "#15 [release 6/18] WORKDIR /var/holoscan\n", "#15 CACHED\n", "\n", "#16 [release 2/18] RUN apt update && apt-get install -y --no-install-recommends --no-install-suggests python3-minimal=3.10.6-1~22.04 libpython3-stdlib=3.10.6-1~22.04 python3=3.10.6-1~22.04 python3-venv=3.10.6-1~22.04 python3-pip=22.0.2+dfsg-* && rm -rf /var/lib/apt/lists/*\n", "#16 CACHED\n", "\n", "#17 [release 3/18] RUN groupadd -f -g 1000 holoscan\n", "#17 CACHED\n", "\n", "#18 [release 9/18] WORKDIR /var/holoscan\n", "#18 CACHED\n", "\n", "#19 [release 10/18] COPY ./pip/requirements.txt /tmp/requirements.txt\n", "#19 DONE 0.3s\n", "\n", "#20 [release 11/18] RUN pip install --upgrade pip\n", "#20 0.789 Defaulting to user installation because normal site-packages is not writeable\n", "#20 0.842 Requirement already satisfied: pip in /usr/lib/python3/dist-packages (22.0.2)\n", "#20 1.003 Collecting pip\n", "#20 1.069 Downloading pip-25.0-py3-none-any.whl (1.8 MB)\n", "#20 1.144 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 26.6 MB/s eta 0:00:00\n", "#20 1.170 Installing collected packages: pip\n", "#20 1.890 Successfully installed pip-25.0\n", "#20 DONE 2.1s\n", "\n", "#21 [release 12/18] RUN pip install --no-cache-dir --user -r /tmp/requirements.txt\n", "#21 0.675 Collecting monai>=1.2.0 (from -r /tmp/requirements.txt (line 1))\n", "#21 0.689 Downloading monai-1.4.0-py3-none-any.whl.metadata (11 kB)\n", "#21 0.906 Collecting Pillow>=8.4.0 (from -r /tmp/requirements.txt (line 2))\n", "#21 0.910 Downloading pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl.metadata (9.1 kB)\n", "#21 0.925 Collecting pydicom>=2.3.0 (from -r /tmp/requirements.txt (line 3))\n", "#21 0.931 Downloading pydicom-3.0.1-py3-none-any.whl.metadata (9.4 kB)\n", "#21 1.037 Collecting highdicom>=0.18.2 (from -r /tmp/requirements.txt (line 4))\n", "#21 1.043 Downloading highdicom-0.24.0-py3-none-any.whl.metadata (4.7 kB)\n", "#21 1.078 Collecting SimpleITK>=2.0.0 (from -r /tmp/requirements.txt (line 5))\n", "#21 1.082 Downloading SimpleITK-2.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (7.9 kB)\n", "#21 1.083 Requirement already satisfied: setuptools>=59.5.0 in /usr/lib/python3/dist-packages (from -r /tmp/requirements.txt (line 6)) (59.6.0)\n", "#21 1.170 Collecting holoscan>=2.9.0 (from -r /tmp/requirements.txt (line 7))\n", "#21 1.176 Downloading holoscan-2.9.0-cp310-cp310-manylinux_2_35_x86_64.whl.metadata (7.3 kB)\n", "#21 1.315 Collecting numpy<2.0,>=1.24 (from monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 1.319 Downloading numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (61 kB)\n", "#21 1.357 Collecting torch>=1.9 (from monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 1.361 Downloading torch-2.6.0-cp310-cp310-manylinux1_x86_64.whl.metadata (28 kB)\n", "#21 1.480 Collecting pyjpegls>=1.0.0 (from highdicom>=0.18.2->-r /tmp/requirements.txt (line 4))\n", "#21 1.486 Downloading pyjpegls-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.5 kB)\n", "#21 1.500 Collecting typing-extensions>=4.0.0 (from highdicom>=0.18.2->-r /tmp/requirements.txt (line 4))\n", "#21 1.504 Downloading typing_extensions-4.12.2-py3-none-any.whl.metadata (3.0 kB)\n", "#21 1.507 Requirement already satisfied: pip>22.0.2 in /home/holoscan/.local/lib/python3.10/site-packages (from holoscan>=2.9.0->-r /tmp/requirements.txt (line 7)) (25.0)\n", "#21 1.518 Collecting cupy-cuda12x<14.0,>=12.2 (from holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 1.523 Downloading cupy_cuda12x-13.3.0-cp310-cp310-manylinux2014_x86_64.whl.metadata (2.7 kB)\n", "#21 1.554 Collecting cloudpickle<4.0,>=3.0 (from holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 1.559 Downloading cloudpickle-3.1.1-py3-none-any.whl.metadata (7.1 kB)\n", "#21 1.588 Collecting python-on-whales<1.0,>=0.60.1 (from holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 1.593 Downloading python_on_whales-0.75.1-py3-none-any.whl.metadata (18 kB)\n", "#21 1.611 Collecting Jinja2<4.0,>=3.1.3 (from holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 1.614 Downloading jinja2-3.1.5-py3-none-any.whl.metadata (2.6 kB)\n", "#21 1.648 Collecting packaging>=23.1 (from holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 1.652 Downloading packaging-24.2-py3-none-any.whl.metadata (3.2 kB)\n", "#21 1.679 Collecting pyyaml<7.0,>=6.0 (from holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 1.682 Downloading PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (2.1 kB)\n", "#21 1.703 Collecting requests<3.0,>=2.31.0 (from holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 1.706 Downloading requests-2.32.3-py3-none-any.whl.metadata (4.6 kB)\n", "#21 1.786 Collecting psutil<7.0,>=6.0.0 (from holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 1.790 Downloading psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (22 kB)\n", "#21 1.873 Collecting wheel-axle-runtime<1.0 (from holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 1.880 Downloading wheel_axle_runtime-0.0.6-py3-none-any.whl.metadata (8.1 kB)\n", "#21 1.944 Collecting fastrlock>=0.5 (from cupy-cuda12x<14.0,>=12.2->holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 1.948 Downloading fastrlock-0.8.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl.metadata (7.7 kB)\n", "#21 1.986 Collecting MarkupSafe>=2.0 (from Jinja2<4.0,>=3.1.3->holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 1.990 Downloading MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.0 kB)\n", "#21 2.005 INFO: pip is looking at multiple versions of pyjpegls to determine which version is compatible with other requirements. This could take a while.\n", "#21 2.005 Collecting pyjpegls>=1.0.0 (from highdicom>=0.18.2->-r /tmp/requirements.txt (line 4))\n", "#21 2.010 Downloading pyjpegls-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.5 kB)\n", "#21 2.016 Downloading pyjpegls-1.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.5 kB)\n", "#21 2.108 Collecting pydantic!=2.0.*,<3,>=2 (from python-on-whales<1.0,>=0.60.1->holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 2.115 Downloading pydantic-2.10.6-py3-none-any.whl.metadata (30 kB)\n", "#21 2.175 Collecting charset-normalizer<4,>=2 (from requests<3.0,>=2.31.0->holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 2.179 Downloading charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (35 kB)\n", "#21 2.190 Collecting idna<4,>=2.5 (from requests<3.0,>=2.31.0->holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 2.195 Downloading idna-3.10-py3-none-any.whl.metadata (10 kB)\n", "#21 2.228 Collecting urllib3<3,>=1.21.1 (from requests<3.0,>=2.31.0->holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 2.235 Downloading urllib3-2.3.0-py3-none-any.whl.metadata (6.5 kB)\n", "#21 2.253 Collecting certifi>=2017.4.17 (from requests<3.0,>=2.31.0->holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 2.256 Downloading certifi-2024.12.14-py3-none-any.whl.metadata (2.3 kB)\n", "#21 2.273 Collecting filelock (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.276 Downloading filelock-3.17.0-py3-none-any.whl.metadata (2.9 kB)\n", "#21 2.304 Collecting networkx (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.311 Downloading networkx-3.4.2-py3-none-any.whl.metadata (6.3 kB)\n", "#21 2.333 Collecting fsspec (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.337 Downloading fsspec-2024.12.0-py3-none-any.whl.metadata (11 kB)\n", "#21 2.381 Collecting nvidia-cuda-nvrtc-cu12==12.4.127 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.385 Downloading nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl.metadata (1.5 kB)\n", "#21 2.395 Collecting nvidia-cuda-runtime-cu12==12.4.127 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.400 Downloading nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl.metadata (1.5 kB)\n", "#21 2.420 Collecting nvidia-cuda-cupti-cu12==12.4.127 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.427 Downloading nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl.metadata (1.6 kB)\n", "#21 2.442 Collecting nvidia-cudnn-cu12==9.1.0.70 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.447 Downloading nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl.metadata (1.6 kB)\n", "#21 2.458 Collecting nvidia-cublas-cu12==12.4.5.8 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.462 Downloading nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl.metadata (1.5 kB)\n", "#21 2.514 Collecting nvidia-cufft-cu12==11.2.1.3 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.518 Downloading nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl.metadata (1.5 kB)\n", "#21 2.526 Collecting nvidia-curand-cu12==10.3.5.147 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.530 Downloading nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl.metadata (1.5 kB)\n", "#21 2.539 Collecting nvidia-cusolver-cu12==11.6.1.9 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.543 Downloading nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl.metadata (1.6 kB)\n", "#21 2.558 Collecting nvidia-cusparse-cu12==12.3.1.170 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.563 Downloading nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl.metadata (1.6 kB)\n", "#21 2.573 Collecting nvidia-cusparselt-cu12==0.6.2 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.578 Downloading nvidia_cusparselt_cu12-0.6.2-py3-none-manylinux2014_x86_64.whl.metadata (6.8 kB)\n", "#21 2.590 Collecting nvidia-nccl-cu12==2.21.5 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.595 Downloading nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl.metadata (1.8 kB)\n", "#21 2.607 Collecting nvidia-nvtx-cu12==12.4.127 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.611 Downloading nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl.metadata (1.7 kB)\n", "#21 2.622 Collecting nvidia-nvjitlink-cu12==12.4.127 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.626 Downloading nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl.metadata (1.5 kB)\n", "#21 2.637 Collecting triton==3.2.0 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.640 Downloading triton-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (1.4 kB)\n", "#21 2.655 Collecting sympy==1.13.1 (from torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.658 Downloading sympy-1.13.1-py3-none-any.whl.metadata (12 kB)\n", "#21 2.686 Collecting mpmath<1.4,>=1.1.0 (from sympy==1.13.1->torch>=1.9->monai>=1.2.0->-r /tmp/requirements.txt (line 1))\n", "#21 2.690 Downloading mpmath-1.3.0-py3-none-any.whl.metadata (8.6 kB)\n", "#21 2.715 Collecting annotated-types>=0.6.0 (from pydantic!=2.0.*,<3,>=2->python-on-whales<1.0,>=0.60.1->holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 2.719 Downloading annotated_types-0.7.0-py3-none-any.whl.metadata (15 kB)\n", "#21 3.298 Collecting pydantic-core==2.27.2 (from pydantic!=2.0.*,<3,>=2->python-on-whales<1.0,>=0.60.1->holoscan>=2.9.0->-r /tmp/requirements.txt (line 7))\n", "#21 3.301 Downloading pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (6.6 kB)\n", "#21 3.324 Downloading monai-1.4.0-py3-none-any.whl (1.5 MB)\n", "#21 3.353 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.5/1.5 MB 65.9 MB/s eta 0:00:00\n", "#21 3.360 Downloading pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl (4.5 MB)\n", "#21 3.405 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 110.9 MB/s eta 0:00:00\n", "#21 3.412 Downloading pydicom-3.0.1-py3-none-any.whl (2.4 MB)\n", "#21 3.435 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.4/2.4 MB 116.6 MB/s eta 0:00:00\n", "#21 3.440 Downloading highdicom-0.24.0-py3-none-any.whl (1.1 MB)\n", "#21 3.455 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.1/1.1 MB 118.9 MB/s eta 0:00:00\n", "#21 3.463 Downloading SimpleITK-2.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (52.4 MB)\n", "#21 3.984 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 52.4/52.4 MB 101.6 MB/s eta 0:00:00\n", "#21 3.991 Downloading holoscan-2.9.0-cp310-cp310-manylinux_2_35_x86_64.whl (41.1 MB)\n", "#21 4.401 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 41.1/41.1 MB 101.5 MB/s eta 0:00:00\n", "#21 4.408 Downloading cloudpickle-3.1.1-py3-none-any.whl (20 kB)\n", "#21 4.415 Downloading cupy_cuda12x-13.3.0-cp310-cp310-manylinux2014_x86_64.whl (90.6 MB)\n", "#21 5.321 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 90.6/90.6 MB 100.4 MB/s eta 0:00:00\n", "#21 5.326 Downloading jinja2-3.1.5-py3-none-any.whl (134 kB)\n", "#21 5.332 Downloading numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (18.2 MB)\n", "#21 5.492 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 18.2/18.2 MB 116.8 MB/s eta 0:00:00\n", "#21 5.499 Downloading packaging-24.2-py3-none-any.whl (65 kB)\n", "#21 5.506 Downloading psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (287 kB)\n", "#21 5.586 Downloading pyjpegls-1.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.7 MB)\n", "#21 5.679 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.7/2.7 MB 28.5 MB/s eta 0:00:00\n", "#21 5.687 Downloading python_on_whales-0.75.1-py3-none-any.whl (114 kB)\n", "#21 5.693 Downloading PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (751 kB)\n", "#21 5.703 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 751.2/751.2 kB 111.7 MB/s eta 0:00:00\n", "#21 5.712 Downloading requests-2.32.3-py3-none-any.whl (64 kB)\n", "#21 5.721 Downloading torch-2.6.0-cp310-cp310-manylinux1_x86_64.whl (766.7 MB)\n", "#21 12.46 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 766.7/766.7 MB 113.4 MB/s eta 0:00:00\n", "#21 12.47 Downloading nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl (363.4 MB)\n", "#21 15.69 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 363.4/363.4 MB 116.3 MB/s eta 0:00:00\n", "#21 15.70 Downloading nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl (13.8 MB)\n", "#21 15.82 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 13.8/13.8 MB 117.0 MB/s eta 0:00:00\n", "#21 15.83 Downloading nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl (24.6 MB)\n", "#21 16.05 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 24.6/24.6 MB 116.4 MB/s eta 0:00:00\n", "#21 16.05 Downloading nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl (883 kB)\n", "#21 16.07 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 883.7/883.7 kB 76.3 MB/s eta 0:00:00\n", "#21 16.07 Downloading nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl (664.8 MB)\n", "#21 21.81 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 664.8/664.8 MB 115.3 MB/s eta 0:00:00\n", "#21 21.82 Downloading nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl (211.5 MB)\n", "#21 23.66 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 211.5/211.5 MB 115.1 MB/s eta 0:00:00\n", "#21 23.67 Downloading nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl (56.3 MB)\n", "#21 24.25 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 56.3/56.3 MB 99.0 MB/s eta 0:00:00\n", "#21 24.25 Downloading nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl (127.9 MB)\n", "#21 25.35 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 127.9/127.9 MB 117.0 MB/s eta 0:00:00\n", "#21 25.36 Downloading nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl (207.5 MB)\n", "#21 27.15 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 207.5/207.5 MB 116.0 MB/s eta 0:00:00\n", "#21 27.16 Downloading nvidia_cusparselt_cu12-0.6.2-py3-none-manylinux2014_x86_64.whl (150.1 MB)\n", "#21 28.47 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 150.1/150.1 MB 115.5 MB/s eta 0:00:00\n", "#21 28.48 Downloading nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl (188.7 MB)\n", "#21 30.18 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 188.7/188.7 MB 111.3 MB/s eta 0:00:00\n", "#21 30.18 Downloading nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl (21.1 MB)\n", "#21 30.37 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 21.1/21.1 MB 117.4 MB/s eta 0:00:00\n", "#21 30.37 Downloading nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl (99 kB)\n", "#21 30.38 Downloading sympy-1.13.1-py3-none-any.whl (6.2 MB)\n", "#21 30.44 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.2/6.2 MB 117.0 MB/s eta 0:00:00\n", "#21 30.45 Downloading triton-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (253.1 MB)\n", "#21 33.27 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 253.1/253.1 MB 89.9 MB/s eta 0:00:00\n", "#21 33.27 Downloading typing_extensions-4.12.2-py3-none-any.whl (37 kB)\n", "#21 33.28 Downloading wheel_axle_runtime-0.0.6-py3-none-any.whl (14 kB)\n", "#21 33.29 Downloading certifi-2024.12.14-py3-none-any.whl (164 kB)\n", "#21 33.29 Downloading charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (146 kB)\n", "#21 33.30 Downloading fastrlock-0.8.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl (53 kB)\n", "#21 33.31 Downloading idna-3.10-py3-none-any.whl (70 kB)\n", "#21 33.31 Downloading MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (20 kB)\n", "#21 33.32 Downloading pydantic-2.10.6-py3-none-any.whl (431 kB)\n", "#21 33.33 Downloading pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.0 MB)\n", "#21 33.41 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.0/2.0 MB 22.9 MB/s eta 0:00:00\n", "#21 33.42 Downloading urllib3-2.3.0-py3-none-any.whl (128 kB)\n", "#21 33.42 Downloading filelock-3.17.0-py3-none-any.whl (16 kB)\n", "#21 33.43 Downloading fsspec-2024.12.0-py3-none-any.whl (183 kB)\n", "#21 33.44 Downloading networkx-3.4.2-py3-none-any.whl (1.7 MB)\n", "#21 33.45 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.7/1.7 MB 114.7 MB/s eta 0:00:00\n", "#21 33.46 Downloading annotated_types-0.7.0-py3-none-any.whl (13 kB)\n", "#21 33.46 Downloading mpmath-1.3.0-py3-none-any.whl (536 kB)\n", "#21 33.48 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 536.2/536.2 kB 115.5 MB/s eta 0:00:00\n", "#21 45.67 Installing collected packages: triton, SimpleITK, nvidia-cusparselt-cu12, mpmath, fastrlock, urllib3, typing-extensions, sympy, pyyaml, pydicom, psutil, Pillow, packaging, nvidia-nvtx-cu12, nvidia-nvjitlink-cu12, nvidia-nccl-cu12, nvidia-curand-cu12, nvidia-cufft-cu12, nvidia-cuda-runtime-cu12, nvidia-cuda-nvrtc-cu12, nvidia-cuda-cupti-cu12, nvidia-cublas-cu12, numpy, networkx, MarkupSafe, idna, fsspec, filelock, cloudpickle, charset-normalizer, certifi, annotated-types, wheel-axle-runtime, requests, pyjpegls, pydantic-core, nvidia-cusparse-cu12, nvidia-cudnn-cu12, Jinja2, cupy-cuda12x, pydantic, nvidia-cusolver-cu12, highdicom, torch, python-on-whales, monai, holoscan\n", "#21 112.5 Successfully installed Jinja2-3.1.5 MarkupSafe-3.0.2 Pillow-11.1.0 SimpleITK-2.4.1 annotated-types-0.7.0 certifi-2024.12.14 charset-normalizer-3.4.1 cloudpickle-3.1.1 cupy-cuda12x-13.3.0 fastrlock-0.8.3 filelock-3.17.0 fsspec-2024.12.0 highdicom-0.24.0 holoscan-2.9.0 idna-3.10 monai-1.4.0 mpmath-1.3.0 networkx-3.4.2 numpy-1.26.4 nvidia-cublas-cu12-12.4.5.8 nvidia-cuda-cupti-cu12-12.4.127 nvidia-cuda-nvrtc-cu12-12.4.127 nvidia-cuda-runtime-cu12-12.4.127 nvidia-cudnn-cu12-9.1.0.70 nvidia-cufft-cu12-11.2.1.3 nvidia-curand-cu12-10.3.5.147 nvidia-cusolver-cu12-11.6.1.9 nvidia-cusparse-cu12-12.3.1.170 nvidia-cusparselt-cu12-0.6.2 nvidia-nccl-cu12-2.21.5 nvidia-nvjitlink-cu12-12.4.127 nvidia-nvtx-cu12-12.4.127 packaging-24.2 psutil-6.1.1 pydantic-2.10.6 pydantic-core-2.27.2 pydicom-3.0.1 pyjpegls-1.4.0 python-on-whales-0.75.1 pyyaml-6.0.2 requests-2.32.3 sympy-1.13.1 torch-2.6.0 triton-3.2.0 typing-extensions-4.12.2 urllib3-2.3.0 wheel-axle-runtime-0.0.6\n", "#21 DONE 113.9s\n", "\n", "#22 [release 13/18] RUN pip install monai-deploy-app-sdk==2.0.0\n", "#22 1.416 Defaulting to user installation because normal site-packages is not writeable\n", "#22 1.603 Collecting monai-deploy-app-sdk==2.0.0\n", "#22 1.654 Downloading monai_deploy_app_sdk-2.0.0-py3-none-any.whl (132 kB)\n", "#22 1.697 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 132.6/132.6 KB 3.6 MB/s eta 0:00:00\n", "#22 1.754 Collecting colorama>=0.4.1\n", "#22 1.759 Downloading colorama-0.4.6-py2.py3-none-any.whl (25 kB)\n", "#22 1.860 Collecting typeguard>=3.0.0\n", "#22 1.865 Downloading typeguard-4.4.1-py3-none-any.whl (35 kB)\n", "#22 1.879 Requirement already satisfied: numpy>=1.21.6 in /home/holoscan/.local/lib/python3.10/site-packages (from monai-deploy-app-sdk==2.0.0) (1.26.4)\n", "#22 1.880 Requirement already satisfied: holoscan~=2.0 in /home/holoscan/.local/lib/python3.10/site-packages (from monai-deploy-app-sdk==2.0.0) (2.9.0)\n", "#22 1.897 Requirement already satisfied: psutil<7.0,>=6.0.0 in /home/holoscan/.local/lib/python3.10/site-packages (from holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (6.1.1)\n", "#22 1.898 Requirement already satisfied: pyyaml<7.0,>=6.0 in /home/holoscan/.local/lib/python3.10/site-packages (from holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (6.0.2)\n", "#22 1.899 Requirement already satisfied: python-on-whales<1.0,>=0.60.1 in /home/holoscan/.local/lib/python3.10/site-packages (from holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (0.75.1)\n", "#22 1.900 Requirement already satisfied: Jinja2<4.0,>=3.1.3 in /home/holoscan/.local/lib/python3.10/site-packages (from holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (3.1.5)\n", "#22 1.901 Requirement already satisfied: packaging>=23.1 in /home/holoscan/.local/lib/python3.10/site-packages (from holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (24.2)\n", "#22 1.902 Requirement already satisfied: cupy-cuda12x<14.0,>=12.2 in /home/holoscan/.local/lib/python3.10/site-packages (from holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (13.3.0)\n", "#22 1.903 Requirement already satisfied: wheel-axle-runtime<1.0 in /home/holoscan/.local/lib/python3.10/site-packages (from holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (0.0.6)\n", "#22 1.903 Requirement already satisfied: requests<3.0,>=2.31.0 in /home/holoscan/.local/lib/python3.10/site-packages (from holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (2.32.3)\n", "#22 1.904 Requirement already satisfied: cloudpickle<4.0,>=3.0 in /home/holoscan/.local/lib/python3.10/site-packages (from holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (3.1.1)\n", "#22 1.987 Collecting pip>22.0.2\n", "#22 1.999 Using cached pip-25.0-py3-none-any.whl (1.8 MB)\n", "#22 2.022 Requirement already satisfied: typing-extensions>=4.10.0 in /home/holoscan/.local/lib/python3.10/site-packages (from typeguard>=3.0.0->monai-deploy-app-sdk==2.0.0) (4.12.2)\n", "#22 2.036 Requirement already satisfied: fastrlock>=0.5 in /home/holoscan/.local/lib/python3.10/site-packages (from cupy-cuda12x<14.0,>=12.2->holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (0.8.3)\n", "#22 2.041 Requirement already satisfied: MarkupSafe>=2.0 in /home/holoscan/.local/lib/python3.10/site-packages (from Jinja2<4.0,>=3.1.3->holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (3.0.2)\n", "#22 2.061 Requirement already satisfied: pydantic!=2.0.*,<3,>=2 in /home/holoscan/.local/lib/python3.10/site-packages (from python-on-whales<1.0,>=0.60.1->holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (2.10.6)\n", "#22 2.069 Requirement already satisfied: certifi>=2017.4.17 in /home/holoscan/.local/lib/python3.10/site-packages (from requests<3.0,>=2.31.0->holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (2024.12.14)\n", "#22 2.070 Requirement already satisfied: urllib3<3,>=1.21.1 in /home/holoscan/.local/lib/python3.10/site-packages (from requests<3.0,>=2.31.0->holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (2.3.0)\n", "#22 2.071 Requirement already satisfied: idna<4,>=2.5 in /home/holoscan/.local/lib/python3.10/site-packages (from requests<3.0,>=2.31.0->holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (3.10)\n", "#22 2.071 Requirement already satisfied: charset-normalizer<4,>=2 in /home/holoscan/.local/lib/python3.10/site-packages (from requests<3.0,>=2.31.0->holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (3.4.1)\n", "#22 2.075 Requirement already satisfied: filelock in /home/holoscan/.local/lib/python3.10/site-packages (from wheel-axle-runtime<1.0->holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (3.17.0)\n", "#22 2.093 Requirement already satisfied: annotated-types>=0.6.0 in /home/holoscan/.local/lib/python3.10/site-packages (from pydantic!=2.0.*,<3,>=2->python-on-whales<1.0,>=0.60.1->holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (0.7.0)\n", "#22 2.094 Requirement already satisfied: pydantic-core==2.27.2 in /home/holoscan/.local/lib/python3.10/site-packages (from pydantic!=2.0.*,<3,>=2->python-on-whales<1.0,>=0.60.1->holoscan~=2.0->monai-deploy-app-sdk==2.0.0) (2.27.2)\n", "#22 2.498 Installing collected packages: typeguard, pip, colorama, monai-deploy-app-sdk\n", "#22 3.427 Successfully installed colorama-0.4.6 monai-deploy-app-sdk-2.0.0 pip-25.0 typeguard-4.4.1\n", "#22 DONE 3.6s\n", "\n", "#23 [release 14/18] COPY ./models /opt/holoscan/models\n", "#23 DONE 0.3s\n", "\n", "#24 [release 15/18] COPY ./map/app.json /etc/holoscan/app.json\n", "#24 DONE 0.0s\n", "\n", "#25 [release 16/18] COPY ./app.config /var/holoscan/app.yaml\n", "#25 DONE 0.1s\n", "\n", "#26 [release 17/18] COPY ./map/pkg.json /etc/holoscan/pkg.json\n", "#26 DONE 0.2s\n", "\n", "#27 [release 18/18] COPY ./app /opt/holoscan/app\n", "#27 DONE 0.1s\n", "\n", "#28 exporting to docker image format\n", "#28 exporting layers\n", "#28 exporting layers 205.6s done\n", "#28 exporting manifest sha256:4da1af22bc19284ede41eafbb2ee59a221a08b1177a65d265463f91f72d9330a 0.0s done\n", "#28 exporting config sha256:bd0c6ea997b6616e1fe7bcad7af9bdf8cd1349b0e7417d1995a634204e10b7db 0.0s done\n", "#28 sending tarball\n", "#28 ...\n", "\n", "#29 importing to docker\n", "#29 loading layer 5a86d5b747ab 276B / 276B\n", "#29 loading layer fa24015991cf 65.54kB / 5.10MB\n", "#29 loading layer d0e9054e123a 557.06kB / 3.33GB\n", "#29 loading layer d0e9054e123a 142.05MB / 3.33GB 6.3s\n", "#29 loading layer d0e9054e123a 317.52MB / 3.33GB 10.4s\n", "#29 loading layer d0e9054e123a 521.40MB / 3.33GB 16.5s\n", "#29 loading layer d0e9054e123a 755.37MB / 3.33GB 20.7s\n", "#29 loading layer d0e9054e123a 955.91MB / 3.33GB 24.9s\n", "#29 loading layer d0e9054e123a 1.18GB / 3.33GB 29.0s\n", "#29 loading layer d0e9054e123a 1.38GB / 3.33GB 33.1s\n", "#29 loading layer d0e9054e123a 1.60GB / 3.33GB 37.1s\n", "#29 loading layer d0e9054e123a 1.85GB / 3.33GB 41.3s\n", "#29 loading layer d0e9054e123a 2.14GB / 3.33GB 45.4s\n", "#29 loading layer d0e9054e123a 2.30GB / 3.33GB 51.6s\n", "#29 loading layer d0e9054e123a 2.34GB / 3.33GB 57.5s\n", "#29 loading layer d0e9054e123a 2.51GB / 3.33GB 63.5s\n", "#29 loading layer d0e9054e123a 2.71GB / 3.33GB 67.6s\n", "#29 loading layer d0e9054e123a 2.99GB / 3.33GB 71.8s\n", "#29 loading layer d0e9054e123a 3.17GB / 3.33GB 78.0s\n", "#29 loading layer 648e09e4cbfd 65.54kB / 3.82MB\n", "#29 loading layer 3d06fe16d5f1 262.14kB / 26.20MB\n", "#29 loading layer 1281a6eddb70 512B / 512B\n", "#29 loading layer 3a64fd65428a 320B / 320B\n", "#29 loading layer 958543fd52f7 300B / 300B\n", "#29 loading layer 2b610be3d181 4.04kB / 4.04kB\n", "#29 loading layer 5a86d5b747ab 276B / 276B 87.6s done\n", "#29 loading layer fa24015991cf 65.54kB / 5.10MB 87.6s done\n", "#29 loading layer d0e9054e123a 3.28GB / 3.33GB 86.9s done\n", "#29 loading layer 648e09e4cbfd 65.54kB / 3.82MB 1.7s done\n", "#29 loading layer 3d06fe16d5f1 262.14kB / 26.20MB 1.0s done\n", "#29 loading layer 1281a6eddb70 512B / 512B 0.5s done\n", "#29 loading layer 3a64fd65428a 320B / 320B 0.5s done\n", "#29 loading layer 958543fd52f7 300B / 300B 0.4s done\n", "#29 loading layer 2b610be3d181 4.04kB / 4.04kB 0.4s done\n", "#29 DONE 87.6s\n", "\n", "#28 exporting to docker image format\n", "#28 sending tarball 129.0s done\n", "#28 DONE 334.7s\n", "\n", "#30 exporting cache to client directory\n", "#30 preparing build cache for export\n", "#30 writing layer sha256:067153055e77a79b3715e3a56caac895ee686a2fa6cadc4423c28d2eb20f0542\n", "#30 writing layer sha256:067153055e77a79b3715e3a56caac895ee686a2fa6cadc4423c28d2eb20f0542 done\n", "#30 writing layer sha256:1a0d52c93099897b518eb6cc6cd0fa3d52ff733e8606b4d8c92675ba9e7101ff done\n", "#30 writing layer sha256:234b866f57e0c5d555af2d87a1857a17ec4ac7e70d2dc6c31ff0a072a4607f24 done\n", "#30 writing layer sha256:255905badeaa82f032e1043580eed8b745c19cd4a2cb7183883ee5a30f851d6d done\n", "#30 writing layer sha256:3713021b02770a720dea9b54c03d0ed83e03a2ef5dce2898c56a327fee9a8bca done\n", "#30 writing layer sha256:3a80776cdc9c9ef79bb38510849c9160f82462d346bf5a8bf29c811391b4e763 done\n", "#30 writing layer sha256:440849e3569a74baf883d1a14010854807280727ba17c36f82beee5b7d5052b2 done\n", "#30 writing layer sha256:46c9c54348df10b0d7700bf932d5de7dc5bf9ab91e685db7086e29e381ff8e12 done\n", "#30 writing layer sha256:4bf64b4a646ee3a3bfe543702f056f9d42e4b3f4cd8465f46a47b0ddc147ecc8\n", "#30 writing layer sha256:4bf64b4a646ee3a3bfe543702f056f9d42e4b3f4cd8465f46a47b0ddc147ecc8 0.6s done\n", "#30 writing layer sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1\n", "#30 writing layer sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 done\n", "#30 writing layer sha256:67b3546b211deefd67122e680c0932886e0b3c6bd6ae0665e3ab25d2d9f0cda0 done\n", "#30 writing layer sha256:695ba418a525cecd1c5442c010ea5f070327d38dfa8f533e63ae845fc3660be8 done\n", "#30 writing layer sha256:7265ec80a3c8c57167f59f1449c2f21828c32e4709977c1b621386bb3184cf5a 0.0s done\n", "#30 writing layer sha256:7ddbbcbbe9e3f16cc9ad8b9a58d58784b79dc5f14ff7c2b510885ba52c22296d 0.0s done\n", "#30 writing layer sha256:95f7cac88539ce722ac1b7fd96100ad34e88797194f42e187db5df91772bcbca 0.0s done\n", "#30 writing layer sha256:980c13e156f90218b216bc6b0430472bbda71c0202804d350c0e16ef02075885 done\n", "#30 writing layer sha256:99298707ae15b7a513faee23c8ff010664939627cce97994f59b6d5cf59ea118\n", "#30 writing layer sha256:99298707ae15b7a513faee23c8ff010664939627cce97994f59b6d5cf59ea118 54.5s done\n", "#30 writing layer sha256:ac52600be001236a2c291a4c5902c915bf5ec9d2441c06d2a54c587b76345847\n", "#30 writing layer sha256:ac52600be001236a2c291a4c5902c915bf5ec9d2441c06d2a54c587b76345847 done\n", "#30 writing layer sha256:aee6cca3099a5220871a6e00732843296f61b693822b537c69c1a7bf14cd1b12 0.1s done\n", "#30 writing layer sha256:bb2407c281c362de0374201b303bd07b66034affb513c87fcc2063aa53df5358 0.1s done\n", "#30 writing layer sha256:bc25d810fc1fd99656c1b07d422e88cdb896508730175bc3ec187b79f3787044\n", "#30 preparing build cache for export 55.7s done\n", "#30 writing layer sha256:bc25d810fc1fd99656c1b07d422e88cdb896508730175bc3ec187b79f3787044 done\n", "#30 writing layer sha256:be0dad9c160128582482df5e64337c99c213a48988d5d12d453bd03bc2a4c1b1 done\n", "#30 writing layer sha256:c94af7742e07c9041104260b79637c243ef8dd25eb4241f06ef1a3899a99f2bd done\n", "#30 writing layer sha256:d339273dfb7fc3b7fd896d3610d360ab9a09ab33a818093cb73b4be7639b6e99 done\n", "#30 writing layer sha256:e12657e78d4fc5b6d4e07266eec8aee83c8dc1ad0e89648bc4a00b088bab0812 0.0s done\n", "#30 writing layer sha256:e4010fcd43e6d2b7dc5e01a744445d8d97ecb37c2488c1e5508b1c26d9dc0832 0.0s done\n", "#30 writing layer sha256:efc9014e2a4cb1e133b80bb4f047e9141e98685eb95b8d2471a8e35b86643e31 done\n", "#30 writing config sha256:6f53ff17759f64b05fccbdc3ebcc89511178905d0639b8c8c51473fa58aa5930 0.0s done\n", "#30 writing cache manifest sha256:fc06123c606626b19ea0c2ed6307971938a11e38874ce5f670ed1a909b55f953 0.0s done\n", "#30 DONE 55.7s\n", "[2025-01-29 14:23:37,936] [INFO] (packager) - Build Summary:\n", "\n", "Platform: x64-workstation/dgpu\n", " Status: Succeeded\n", " Docker Tag: mednist_app-x64-workstation-dgpu-linux-amd64:1.0\n", " Tarball: None\n" ] } ], "source": [ "tag_prefix = \"mednist_app\"\n", "\n", "!monai-deploy package \"mednist_app/mednist_classifier_monaideploy.py\" -m {models_folder} -c \"mednist_app/app.yaml\" -t {tag_prefix}:1.0 --platform x86_64 -l DEBUG" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ ":::{note}\n", "Building a MONAI Application Package (Docker image) can take time. Use `-l DEBUG` option if you want to see the progress.\n", "\n", ":::\n", "\n", "We can see that the Docker image is created." ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "mednist_app-x64-workstation-dgpu-linux-amd64 1.0 bd0c6ea997b6 6 minutes ago 8.64GB\n" ] } ], "source": [ "!docker image ls | grep {tag_prefix}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Executing packaged app locally\n", "\n", "We can choose to display and export the MAP manifests, but in this example, we will just run the MAP through MONAI Application Runner." ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[2025-01-29 14:23:40,347] [INFO] (runner) - Checking dependencies...\n", "[2025-01-29 14:23:40,348] [INFO] (runner) - --> Verifying if \"docker\" is installed...\n", "\n", "[2025-01-29 14:23:40,349] [INFO] (runner) - --> Verifying if \"docker-buildx\" is installed...\n", "\n", "[2025-01-29 14:23:40,353] [INFO] (runner) - --> Verifying if \"mednist_app-x64-workstation-dgpu-linux-amd64:1.0\" is available...\n", "\n", "[2025-01-29 14:23:40,453] [INFO] (runner) - Reading HAP/MAP manifest...\n", "Successfully copied 2.56kB to /tmp/tmpnhktw736/app.json\n", "Successfully copied 2.05kB to /tmp/tmpnhktw736/pkg.json\n", "8955ab5e4aef6b83355220e70e421a94b58670eb9ad05cecbba971905c1833b2\n", "[2025-01-29 14:23:40,759] [INFO] (runner) - --> Verifying if \"nvidia-ctk\" is installed...\n", "\n", "[2025-01-29 14:23:40,769] [INFO] (runner) - --> Verifying \"nvidia-ctk\" version...\n", "\n", "[2025-01-29 14:23:41,150] [INFO] (common) - Launching container (30bd3cacd8fc) using image 'mednist_app-x64-workstation-dgpu-linux-amd64:1.0'...\n", " container name: modest_gould\n", " host name: mingq-dt\n", " network: host\n", " user: 1000:1000\n", " ulimits: memlock=-1:-1, stack=67108864:67108864\n", " cap_add: CAP_SYS_PTRACE\n", " ipc mode: host\n", " shared memory size: 67108864\n", " devices: \n", " group_add: 44\n", "2025-01-29 22:23:42 [INFO] Launching application python3 /opt/holoscan/app/mednist_classifier_monaideploy.py ...\n", "\n", "[info] [fragment.cpp:599] Loading extensions from configs...\n", "\n", "[info] [gxf_executor.cpp:264] Creating context\n", "\n", "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'output_folder'\n", "\n", "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'image'\n", "\n", "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'study_selected_series_list'\n", "\n", "[info] [gxf_executor.cpp:1797] creating input IOSpec named 'text'\n", "\n", "[info] [gxf_executor.cpp:2208] Activating Graph...\n", "\n", "[info] [gxf_executor.cpp:2238] Running Graph...\n", "\n", "[info] [gxf_executor.cpp:2240] Waiting for completion...\n", "\n", "[info] [greedy_scheduler.cpp:191] Scheduling 3 entities\n", "\n", "/home/holoscan/.local/lib/python3.10/site-packages/monai/data/meta_tensor.py:116: UserWarning: The given NumPy array is not writable, and PyTorch does not support non-writable tensors. This means writing to this tensor will result in undefined behavior. You may want to copy the array to protect its data or make it writable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at /pytorch/torch/csrc/utils/tensor_numpy.cpp:203.)\n", "\n", " return torch.as_tensor(x, *args, **_kwargs).as_subclass(cls)\n", "\n", "WARNING:pydicom:'Dataset.is_implicit_VR' will be removed in v4.0, set the Transfer Syntax UID or use the 'implicit_vr' argument with Dataset.save_as() or dcmwrite() instead\n", "\n", "WARNING:pydicom:'Dataset.is_little_endian' will be removed in v4.0, set the Transfer Syntax UID or use the 'little_endian' argument with Dataset.save_as() or dcmwrite() instead\n", "\n", "WARNING:pydicom:Invalid value for VR UI: 'xyz'. Please see for allowed values for each VR.\n", "\n", "/home/holoscan/.local/lib/python3.10/site-packages/pydicom/valuerep.py:440: UserWarning: Invalid value for VR UI: 'xyz'. Please see for allowed values for each VR.\n", "\n", " warn_and_log(msg)\n", "\n", "WARNING:pydicom:'write_like_original' is deprecated and will be removed in v4.0, please use 'enforce_file_format' instead\n", "\n", "[info] [greedy_scheduler.cpp:372] Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.\n", "\n", "[info] [greedy_scheduler.cpp:401] Scheduler finished.\n", "\n", "[info] [gxf_executor.cpp:2243] Deactivating Graph...\n", "\n", "[info] [gxf_executor.cpp:2251] Graph execution finished.\n", "\n", "[info] [gxf_executor.cpp:294] Destroying context\n", "\n", "AbdomenCT\n", "\n", "[2025-01-29 14:23:51,575] [INFO] (common) - Container 'modest_gould'(30bd3cacd8fc) exited.\n" ] } ], "source": [ "# Clear the output folder and run the MAP. The input is expected to be a folder.\n", "!rm -rf $HOLOSCAN_OUTPUT_PATH\n", "!monai-deploy run -i$HOLOSCAN_INPUT_PATH -o $HOLOSCAN_OUTPUT_PATH mednist_app-x64-workstation-dgpu-linux-amd64:1.0" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\"AbdomenCT\"" ] } ], "source": [ "!cat $HOLOSCAN_OUTPUT_PATH/output.json" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Note**: Please execute the following script once the exercise is done." ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [], "source": [ "# Remove data files which is in the temporary folder\n", "if directory is None:\n", " shutil.rmtree(root_dir)" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.12" } }, "nbformat": 4, "nbformat_minor": 4 }