From d6e252ec10f5fc6e1eae43c61e660ade0d47d3e9 Mon Sep 17 00:00:00 2001 From: "R. Garcia-Dias" Date: Thu, 11 Jun 2026 08:38:53 +0100 Subject: [PATCH 01/10] fix: update runner.sh and endoscopic notebook for MONAI 1.6 compatibility Add msd_crossval_datalist_generator.ipynb and hovernet_infer_compare.ipynb to doesnt_contain_max_epochs (inference/datalist notebooks with no training loop). Skip image_restoration.ipynb until monai.networks.nets.Restormer is merged into the dev branch. Fix endoscopic_inbody_classification notebook: pass return_state_dict=False to monai.bundle.load so it returns an nn.Module rather than a raw OrderedDict, which caused AttributeError on .train() with MONAI 1.6 API. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: R. Garcia-Dias --- .../endoscopic_inbody_classification.ipynb | 102 +----------------- runner.sh | 3 + 2 files changed, 7 insertions(+), 98 deletions(-) diff --git a/computer_assisted_intervention/endoscopic_inbody_classification.ipynb b/computer_assisted_intervention/endoscopic_inbody_classification.ipynb index fb97c7f47..fc0a0e930 100644 --- a/computer_assisted_intervention/endoscopic_inbody_classification.ipynb +++ b/computer_assisted_intervention/endoscopic_inbody_classification.ipynb @@ -343,105 +343,11 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "4784fe9e", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "name endoscopic_inbody_classification\n", - "version None\n", - "bundle_dir .\n", - "source github\n", - "repo None\n", - "url None\n", - "remove_prefix monai_\n", - "progress True\n", - "2023-09-07 12:17:23,537 - INFO - --- input summary of monai.bundle.scripts.download ---\n", - "2023-09-07 12:17:23,539 - INFO - > name: 'endoscopic_inbody_classification'\n", - "2023-09-07 12:17:23,540 - INFO - > bundle_dir: PosixPath('.')\n", - "2023-09-07 12:17:23,540 - INFO - > source: 'github'\n", - "2023-09-07 12:17:23,541 - INFO - > remove_prefix: 'monai_'\n", - "2023-09-07 12:17:23,542 - INFO - > progress: True\n", - "2023-09-07 12:17:23,542 - INFO - ---\n", - "\n", - "\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "endoscopic_inbody_classification_v0.4.4.zip: 185MB [00:09, 21.4MB/s] \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-09-07 12:17:33,469 - INFO - Downloaded: endoscopic_inbody_classification_v0.4.4.zip\n", - "2023-09-07 12:17:33,469 - INFO - Expected md5 is None, skip md5 check for file endoscopic_inbody_classification_v0.4.4.zip.\n", - "2023-09-07 12:17:33,470 - INFO - Writing into directory: ..\n", - "workflow_name None\n", - "config_file endoscopic_inbody_classification/configs/train.json\n", - "workflow_type train\n", - "2023-09-07 12:17:34,468 - INFO - --- input summary of monai.bundle.scripts.run ---\n", - "2023-09-07 12:17:34,468 - INFO - > config_file: 'endoscopic_inbody_classification/configs/train.json'\n", - "2023-09-07 12:17:34,469 - INFO - > workflow_type: 'train'\n", - "2023-09-07 12:17:34,469 - INFO - ---\n", - "\n", - "\n", - "2023-09-07 12:17:34,470 - INFO - Setting logging properties based on config: endoscopic_inbody_classification/configs/logging.conf.\n", - "2023-09-07 12:17:34,701 - INFO - 'dst' model updated: 384 of 384 variables.\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 5/5 [00:02<00:00, 2.25it/s]\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "max_epochs = 5\n", - "\n", - "pretrained_model = monai.bundle.load(name=\"endoscopic_inbody_classification\", bundle_dir=\"./\")\n", - "\n", - "pretrained_model.train()\n", - "losses = []\n", - "\n", - "for _ in trange(max_epochs):\n", - " epoch_loss = 0\n", - " for data in train_dataloader:\n", - " inputs, labels = data[\"image\"].to(device), data[\"label\"].to(device)\n", - " optimizer.zero_grad()\n", - " predictions = pretrained_model(inputs)\n", - " loss_iter = loss(predictions, labels)\n", - " loss_iter.backward()\n", - " optimizer.step()\n", - " epoch_loss += loss_iter.item()\n", - " losses.append(epoch_loss / len(train_dataloader))\n", - "\n", - "fig, ax = plt.subplots(1, 1, figsize=(6, 6), facecolor=\"white\")\n", - "ax.set_xlabel(\"Epoch\")\n", - "epochs = list(range(len(losses)))\n", - "ax.plot(epochs, losses, label=\"loss\")\n", - "plt.legend()\n", - "plt.show()" - ] + "outputs": [], + "source": "max_epochs = 5\n\n# return_state_dict=False returns the nn.Module rather than a raw OrderedDict (MONAI 1.6 API)\npretrained_model = monai.bundle.load(name=\"endoscopic_inbody_classification\", bundle_dir=\"./\", return_state_dict=False)\n\npretrained_model.train()\nlosses = []\n\nfor _ in trange(max_epochs):\n epoch_loss = 0\n for data in train_dataloader:\n inputs, labels = data[\"image\"].to(device), data[\"label\"].to(device)\n optimizer.zero_grad()\n predictions = pretrained_model(inputs)\n loss_iter = loss(predictions, labels)\n loss_iter.backward()\n optimizer.step()\n epoch_loss += loss_iter.item()\n losses.append(epoch_loss / len(train_dataloader))\n\nfig, ax = plt.subplots(1, 1, figsize=(6, 6), facecolor=\"white\")\nax.set_xlabel(\"Epoch\")\nepochs = list(range(len(losses)))\nax.plot(epochs, losses, label=\"loss\")\nplt.legend()\nplt.show()" }, { "cell_type": "markdown", @@ -501,4 +407,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/runner.sh b/runner.sh index 3296299fd..0474f124a 100755 --- a/runner.sh +++ b/runner.sh @@ -84,6 +84,8 @@ doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" maisi_inference_tut doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" realism_diversity_metrics.ipynb) doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" omniverse_integration.ipynb) doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" hugging_face_pipeline_for_monai.ipynb) +doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" msd_crossval_datalist_generator.ipynb) # inference/datalist-only notebook, no training loop +doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" hovernet_infer_compare.ipynb) # inference-only notebook # Execution of the notebook in these folders / with the filename cannot be automated skip_run_papermill=() @@ -135,6 +137,7 @@ skip_run_papermill=("${skip_run_papermill[@]}" .*learn2reg_oasis_unpaired_brain_ skip_run_papermill=("${skip_run_papermill[@]}" .*finetune_vista3d_for_hugging_face_pipeline.ipynb*) skip_run_papermill=("${skip_run_papermill[@]}" .*TCIA_PROSTATEx_Prostate_MRI_Anatomy_Model.ipynb*) # https://github.com/Project-MONAI/tutorials/issues/2029 skip_run_papermill=("${skip_run_papermill[@]}" .*maisi_inference_tutorial.ipynb*) +skip_run_papermill=("${skip_run_papermill[@]}" .*image_restoration.ipynb*) # monai.networks.nets.restormer not yet in dev branch # output formatting separator="" From 7877af80ac0042ca05645c88dc62a1b0b63a503c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 07:40:40 +0000 Subject: [PATCH 02/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci Signed-off-by: R. Garcia-Dias --- .../endoscopic_inbody_classification.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/computer_assisted_intervention/endoscopic_inbody_classification.ipynb b/computer_assisted_intervention/endoscopic_inbody_classification.ipynb index fc0a0e930..95d96f691 100644 --- a/computer_assisted_intervention/endoscopic_inbody_classification.ipynb +++ b/computer_assisted_intervention/endoscopic_inbody_classification.ipynb @@ -407,4 +407,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} From a862ed1ac2cffdd99f24b45e2c12906ccd1a1a8a Mon Sep 17 00:00:00 2001 From: "R. Garcia-Dias" Date: Thu, 11 Jun 2026 09:12:54 +0100 Subject: [PATCH 03/10] fix: apply PEP8 autofix to 3 notebooks (E225/E231 whitespace violations) - competitions/MICCAI/surgtoolloc/preprocess_detect_scene_and_split_fold.ipynb: E225 - deep_atlas/deep_atlas_tutorial.ipynb: E225 - modules/interpretability/class_lung_lesion.ipynb: E231 in f-string indexing Signed-off-by: R. Garcia-Dias --- .../preprocess_detect_scene_and_split_fold.ipynb | 2 +- deep_atlas/deep_atlas_tutorial.ipynb | 6 +++--- modules/interpretability/class_lung_lesion.ipynb | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/competitions/MICCAI/surgtoolloc/preprocess_detect_scene_and_split_fold.ipynb b/competitions/MICCAI/surgtoolloc/preprocess_detect_scene_and_split_fold.ipynb index f83c5881b..16a55bcc3 100644 --- a/competitions/MICCAI/surgtoolloc/preprocess_detect_scene_and_split_fold.ipynb +++ b/competitions/MICCAI/surgtoolloc/preprocess_detect_scene_and_split_fold.ipynb @@ -398,7 +398,7 @@ ], "source": [ "for lb in labels:\n", - " print(f\"{lb:30} {df_scene[df_scene[lb]>0].scene.nunique()}\")" + " print(f\"{lb:30} {df_scene[df_scene[lb] > 0].scene.nunique()}\")" ] }, { diff --git a/deep_atlas/deep_atlas_tutorial.ipynb b/deep_atlas/deep_atlas_tutorial.ipynb index 580e2c345..799a85fd3 100644 --- a/deep_atlas/deep_atlas_tutorial.ipynb +++ b/deep_atlas/deep_atlas_tutorial.ipynb @@ -1255,7 +1255,7 @@ "val_interval = 5\n", "\n", "for epoch_number in range(max_epochs):\n", - " print(f\"Epoch {epoch_number+1}/{max_epochs}:\")\n", + " print(f\"Epoch {epoch_number + 1}/{max_epochs}:\")\n", "\n", " seg_net.train()\n", " losses = []\n", @@ -1797,7 +1797,7 @@ "best_reg_validation_loss = float(\"inf\")\n", "\n", "for epoch_number in range(max_epochs):\n", - " print(f\"Epoch {epoch_number+1}/{max_epochs}:\")\n", + " print(f\"Epoch {epoch_number + 1}/{max_epochs}:\")\n", "\n", " # ------------------------------------------------\n", " # reg_net training, with seg_net frozen\n", @@ -2320,7 +2320,7 @@ "preview_image(det, normalize_by=\"slice\", threshold=0)\n", "loss = lncc_loss(example_warped_image, img12[:, [0], :, :, :]).item()\n", "print(f\"Similarity loss: {loss}\")\n", - "print(f\"number of folds: {(det<=0).sum()}\")\n", + "print(f\"number of folds: {(det <= 0).sum()}\")\n", "\n", "del reg_net_example_output, img12, example_warped_image\n", "torch.cuda.empty_cache()" diff --git a/modules/interpretability/class_lung_lesion.ipynb b/modules/interpretability/class_lung_lesion.ipynb index f6afb0c8b..246ee4257 100644 --- a/modules/interpretability/class_lung_lesion.ipynb +++ b/modules/interpretability/class_lung_lesion.ipynb @@ -680,8 +680,8 @@ " name += \"lesion\" if label == 1 else \"non-lesion\"\n", " name += \"\\npred: \"\n", " name += \"lesion\" if pred_label == 1 else \"non-lesion\"\n", - " name += f\"\\nlesion: {y_pred[0,1]:.3}\"\n", - " name += f\"\\nnon-lesion: {y_pred[0,0]:.3}\"\n", + " name += f\"\\nlesion: {y_pred[0, 1]:.3}\"\n", + " name += f\"\\nnon-lesion: {y_pred[0, 0]:.3}\"\n", "\n", " # run CAM\n", " cam_result = cam(x=image, class_idx=None)\n", From 252b9d697bfa3b8db81be60bd3cb96b942973605 Mon Sep 17 00:00:00 2001 From: "R. Garcia-Dias" Date: Thu, 11 Jun 2026 10:26:37 +0100 Subject: [PATCH 04/10] Fix bundle/05_spleen_segmentation_lightning: upgrade pytorch-lightning pin pytorch-lightning~=2.0.0 (2.0.9) imports mlflow at module level via pytorch_lightning.loggers.mlflow; mlflow 3.13.0 fails to initialize under Python 3.12, making `import pytorch_lightning` fail in the training subprocess. pytorch-lightning>=2.1 uses lazy mlflow imports; confirmed working with 2.6.5: training and evaluation run to completion. Also documents the root cause in diagnose_1_6_release.md: two-part R7 issue (fd-limit download truncation + mlflow/pl import chain). Signed-off-by: R. Garcia-Dias --- bundle/05_spleen_segmentation_lightning.ipynb | 4 +- diagnose_1_6_release.md | 450 ++++++++++++++++++ 2 files changed, 452 insertions(+), 2 deletions(-) create mode 100644 diagnose_1_6_release.md diff --git a/bundle/05_spleen_segmentation_lightning.ipynb b/bundle/05_spleen_segmentation_lightning.ipynb index 68af68eca..43cc966f8 100644 --- a/bundle/05_spleen_segmentation_lightning.ipynb +++ b/bundle/05_spleen_segmentation_lightning.ipynb @@ -38,7 +38,7 @@ "outputs": [], "source": [ "!python -c \"import monai\" || pip install -q \"monai-weekly[ignite,pyyaml]\"\n", - "!pip install -q pytorch-lightning~=2.0.0" + "!pip install -q pytorch-lightning>=2.1" ] }, { @@ -855,7 +855,7 @@ "execution_count": 11, "id": "c5ba337d-a5b0-47de-9ae2-1554a2cb4f86", "metadata": {}, - "outputs": [ + "outputs": [ { "name": "stdout", "output_type": "stream", diff --git a/diagnose_1_6_release.md b/diagnose_1_6_release.md new file mode 100644 index 000000000..261a37d5b --- /dev/null +++ b/diagnose_1_6_release.md @@ -0,0 +1,450 @@ +# MONAI 1.6 Release — Tutorial Test Diagnostics + +**Date:** 2026-06-10 +**Image:** `monai_1_6:latest` (`nvcr.io/nvidia/pytorch:25.03-py3` base) +**GPU:** NVIDIA RTX 5090 (Blackwell, SM_120, CUDA 12.8) +**Python:** 3.12 · PyTorch 2.7.0a0 (nv25.03) · NumPy 1.26.4 +**Runner:** `bash runner.sh` (all tutorials) +**Log:** `runner_output.logs` + +--- + +## Summary + +| Category | Count | Notebooks | +|---|---|---| +| **Passed** | ~150 | — | +| **Failed — ExecutionError** | 91 | Papermill failed; specific errors not in log (stderr not captured) | +| **Failed — PEP8** | 3 | Style violations caught by flake8 | +| **Failed — MissingKeyword** | 1 | `max_epochs` not found and not exempted | +| **Total failures** | **94 events / 93 notebooks** | `class_lung_lesion.ipynb` has both PEP8 + ExecutionError | + +> **Important — missing error details:** The runner was invoked without `2>&1`. Papermill writes +> progress and tracebacks to **stderr**, which was not redirected to `runner_output.logs`. All +> 91 ExecutionError notebooks show `papermill … -k python3` then immediately `Check failed!` in +> the log with no further detail. To see actual errors, re-run with: +> ```bash +> bash runner.sh 2>&1 | tee runner_output.logs +> ``` +> Or for a single notebook: +> ```bash +> docker --context default run --gpus all --rm \ +> --entrypoint bash -e NVIDIA_DISABLE_REQUIRE=true \ +> --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 \ +> -v /data/rgd/tutorials:/opt/tutorials \ +> monai_1_6:latest \ +> -c "cd /opt/tutorials && bash runner.sh --verbose -t 2>&1" +> ``` + +--- + +## Category 1 — PEP8 Violations (3 notebooks) + +The flake8 check (run via `jupytext`) fails before papermill is even invoked. +Fix with `bash runner.sh --autofix -t ` or apply manually. + +| Notebook | Error | Location | +|---|---|---| +| `competitions/MICCAI/surgtoolloc/preprocess_detect_scene_and_split_fold.ipynb` | E225 missing whitespace around operator | `stdin:160:44` | +| `deep_atlas/deep_atlas_tutorial.ipynb` | E225 missing whitespace around operator | `stdin:1353:31` | +| `modules/interpretability/class_lung_lesion.ipynb` | E231 missing whitespace after `,` (two occurrences in f-strings `y_pred[0,1]` / `y_pred[0,0]`) | `stdin:358:34`, `stdin:359:38` | + +--- + +## Category 2 — MissingKeyword (1 notebook) + +The runner requires all training notebooks to declare `max_epochs` (so it can reduce it to 1 +for fast CI). Notebooks that don't have it must be added to the `doesnt_contain_max_epochs` +exemption list in `runner.sh`. + +| Notebook | Detail | +|---|---| +| `auto3dseg/notebooks/msd_crossval_datalist_generator.ipynb` | `max_epochs` keyword absent; not in `doesnt_contain_max_epochs` list | + +**Fix options:** +- Add the notebook filename to `doesnt_contain_max_epochs` in `runner.sh` (line ~32), or +- Add a `max_epochs = 1` cell/variable to the notebook. + +--- + +## Category 3 — ExecutionError (91 notebooks) + +Papermill returned a non-zero exit code. Specific errors are in stderr (not captured in +`runner_output.logs`). Sub-grouped by likely root cause. + +### 3a — Generative model training (30 notebooks) + +These notebooks train diffusion/VAE/GAN models. Even with `max_epochs = 1`, they likely fail +because they need either: +- Pre-trained checkpoint files not present in the container, or +- Custom dataset paths (e.g. BraTS, TCIA) not mounted. + +| Notebook | +|---| +| `generation/2d_autoencoderkl/2d_autoencoderkl_tutorial.ipynb` | +| `generation/2d_ddpm/2d_ddpm_compare_schedulers.ipynb` | +| `generation/2d_ddpm/2d_ddpm_inpainting.ipynb` | +| `generation/2d_ddpm/2d_ddpm_tutorial.ipynb` | +| `generation/2d_ddpm/2d_ddpm_tutorial_ignite.ipynb` | +| `generation/2d_ddpm/2d_ddpm_tutorial_v_prediction.ipynb` | +| `generation/2d_diffusion_autoencoder/2d_diffusion_autoencoder_tutorial.ipynb` | +| `generation/2d_ldm/2d_ldm_tutorial.ipynb` | +| `generation/2d_super_resolution/2d_sd_super_resolution.ipynb` | +| `generation/2d_super_resolution/2d_sd_super_resolution_lightning.ipynb` | +| `generation/2d_vqgan/2d_vqgan_tutorial.ipynb` | +| `generation/2d_vqvae/2d_vqvae_tutorial.ipynb` | +| `generation/2d_vqvae_transformer/2d_vqvae_transformer_tutorial.ipynb` | +| `generation/3d_autoencoderkl/3d_autoencoderkl_tutorial.ipynb` | +| `generation/3d_ddpm/3d_ddpm_tutorial.ipynb` | +| `generation/3d_ldm/3d_ldm_tutorial.ipynb` | +| `generation/3d_vqvae/3d_vqvae_tutorial.ipynb` | +| `generation/anomaly_detection/2d_classifierfree_guidance_anomalydetection_tutorial.ipynb` | +| `generation/anomaly_detection/anomaly_detection_with_transformers.ipynb` | +| `generation/anomaly_detection/anomalydetection_tutorial_classifier_guidance.ipynb` | +| `generation/classifier_free_guidance/2d_ddpm_classifier_free_guidance_tutorial.ipynb` | +| `generation/controlnet/2d_controlnet.ipynb` | +| `generation/image_to_image_translation/tutorial_segmentation_with_ddpm.ipynb` | +| `generation/maisi/data/mask_augmentation_example.ipynb` | +| `generation/maisi/maisi_train_controlnet_tutorial.ipynb` | +| `generation/maisi/maisi_train_diff_unet_tutorial.ipynb` | +| `generation/maisi/maisi_train_vae_tutorial.ipynb` | +| `generation/realism_diversity_metrics/realism_diversity_metrics.ipynb` | +| `generation/spade_gan/spade_gan.ipynb` | +| `generation/spade_ldm/spade_ldm_brats.ipynb` | + +### 3b — 3D segmentation / AutoSeg training (6 notebooks) + +These notebooks train segmentation models (Spleen, BraTS, VISTA-3D). They need the +corresponding datasets to be available at the paths assumed by the notebook. + +| Notebook | +|---| +| `3d_segmentation/spleen_segmentation_3d_lightning.ipynb` | +| `3d_segmentation/unet_segmentation_3d_ignite.ipynb` | +| `auto3dseg/notebooks/auto3dseg_autorunner_ref_api.ipynb` | +| `auto3dseg/notebooks/auto3dseg_hello_world.ipynb` | +| `auto3dseg/notebooks/ensemble_byoc.ipynb` | +| `vista_3d/vista3d_spleen_finetune.ipynb` | + +### 3c — External service / tool dependency (7 notebooks) + +These notebooks require external services (tracking servers, serving frameworks, or desktop +applications) that are not available inside the Docker container. + +| Notebook | Service needed | +|---|---| +| `experiment_management/bundle_integrate_mlflow.ipynb` | MLflow tracking server | +| `experiment_management/spleen_segmentation_mlflow.ipynb` | MLflow tracking server | +| `experiment_management/spleen_segmentation_aim.ipynb` | AIM tracking server | +| `deployment/bentoml/mednist_classifier_bentoml.ipynb` | BentoML serving framework | +| `monailabel/monailabel_vista2d_cell_segmentation_CellProfiler.ipynb` | MONAILabel server + CellProfiler | +| `modules/omniverse/omniverse_integration.ipynb` | NVIDIA Omniverse (desktop app) | +| `hugging_face/hugging_face_pipeline_for_monai.ipynb` | HuggingFace model hub / `transformers` pipeline | + +### 3d — MONAI core modules / data loading (48 notebooks) + +These are general-purpose tutorials covering transforms, networks, datasets, and workflows. +Likely failures are data download errors (missing network access or cached data), or import +errors from packages that changed API between versions. Requires `2>&1` re-run to diagnose. + +| Notebook | +|---| +| `2d_regression/image_restoration.ipynb` | +| `bundle/05_spleen_segmentation_lightning.ipynb` | +| `computer_assisted_intervention/endoscopic_inbody_classification.ipynb` | +| `microscopy/multichannel_microscopy_classification.ipynb` | +| `modules/2d_inference_3d_volume.ipynb` | +| `modules/2d_slices_from_3d_sampling.ipynb` | +| `modules/2d_slices_from_3d_training.ipynb` | +| `modules/3d_image_transforms.ipynb` | +| `modules/UNet_input_size_constraints.ipynb` | +| `modules/autoencoder_mednist.ipynb` | +| `modules/batch_output_transform.ipynb` | +| `modules/bending_energy_diffusion_loss_notes.ipynb` | +| `modules/cross_validation_models_ensemble.ipynb` | +| `modules/csv_datasets.ipynb` | +| `modules/decollate_batch.ipynb` | +| `modules/developer_guide.ipynb` | +| `modules/dice_loss_metric_notes.ipynb` | +| `modules/image_dataset.ipynb` | +| `modules/integrate_3rd_party_transforms.ipynb` | +| `modules/interpretability/cats_and_dogs.ipynb` | +| `modules/interpretability/class_lung_lesion.ipynb` *(also PEP8)* | +| `modules/interpretability/covid_classification.ipynb` | +| `modules/inverse_transforms_and_test_time_augmentations.ipynb` | +| `modules/jupyter_utils.ipynb` | +| `modules/layer_wise_learning_rate.ipynb` | +| `modules/lazy_resampling_benchmark.ipynb` | +| `modules/lazy_resampling_compose.ipynb` | +| `modules/lazy_resampling_functional.ipynb` | +| `modules/learning_rate.ipynb` | +| `modules/load_medical_images.ipynb` | +| `modules/mednist_GAN_tutorial.ipynb` | +| `modules/mednist_GAN_workflow_array.ipynb` | +| `modules/mednist_GAN_workflow_dict.ipynb` | +| `modules/network_api.ipynb` | +| `modules/network_contraints/unet_plusplus.ipynb` | +| `modules/nifti_read_example.ipynb` | +| `modules/postprocessing_transforms.ipynb` | +| `modules/public_datasets.ipynb` | +| `modules/resample_benchmark.ipynb` | +| `modules/tcia_csv_processing.ipynb` | +| `modules/torch_compile.ipynb` | +| `modules/transforms_demo_2d.ipynb` | +| `modules/transforms_metatensor.ipynb` | +| `modules/varautoencoder_mednist.ipynb` | +| `modules/workflow_profiling.ipynb` | +| `patch_inferer/modular_patch_inferer.ipynb` | +| `pathology/tumor_detection/ignite/profiling_camelyon_pipeline.ipynb` | + +--- + +## Environment Notes + +Key changes made to the Docker image to reach this test run (from baseline `nvcr.io/nvidia/pytorch:25.03-py3`): + +| Issue | Fix applied | +|---|---| +| `No module named pkg_resources` during build (MetricsReloaded, segment-anything) | `PIP_CONSTRAINT` with `setuptools<71` propagates to pip's isolated build envs | +| RTX 5090 SM_120 not supported | Rebased from `nvcr.io/nvidia/pytorch:24.10-py3` → `25.03-py3` | +| `torch.patch` for ONNX bug (24.10 only) | Removed | +| Python 3.12 markers excluded `cucim`, `transformers`, `onnxruntime` | Removed `python_version <= '3.10'` caps in `requirements-dev.txt` | +| Container's `jupytext==1.16.7` blocked tutorial runner | Rebuilt `/etc/pip/constraint.txt`, kept only `numpy==1.26.4` + `setuptools<71` | +| Container's `isort==6.0.1` conflicted with MONAI's `isort<6.0` | Same constraint file rebuild | +| NumPy 2.x broke PyTorch's C-extension bridge in DataLoader workers | `numpy==1.26.4` pin retained (nv25.03 PyTorch compiled against NumPy 1.x) | +| GPU allowlist check (`NVIDIA_DISABLE_REQUIRE`) | Launch with `-e NVIDIA_DISABLE_REQUIRE=true --entrypoint bash` | + +### Working docker run command + +```bash +docker --context default run --gpus all --rm \ + --entrypoint bash \ + -e NVIDIA_DISABLE_REQUIRE=true \ + --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 --ulimit nofile=65536:65536 \ + -v /data/rgd/tutorials:/opt/tutorials \ + -v /data/rgd/MONAI:/opt/monai \ + monai_1_6:latest \ + -c "cd /opt/tutorials && bash runner.sh -t 2>&1" +``` + +> `--ulimit nofile=65536:65536` is required for `modules/public_datasets.ipynb` (DataLoader +> workers pass file descriptors via Unix sockets; the default container limit of 1024 is too low). +> `2>&1` captures papermill tracebacks that would otherwise be invisible. + +--- + +## Rerun Results (stderr captured) — 2026-06-11 + +A targeted rerun of the 80 "our-only" failing notebooks was performed with +`2>&1 | tee` to capture papermill tracebacks. +Script: `run_our_only.sh` | Log: `runner_output_our_only.logs` + +### Outcome + +| Result | Count | +|---|---| +| Passed | **65** | +| Failed | **15** | +| Total targeted | 80 | + +**All 30 generation notebooks passed** (they download their own small synthetic datasets, +writing ~3.7 GB to `tutorials/generation/`). The unknown "ExecutionError" group is now +fully explained. + +### The 15 remaining failures + +#### Group R1 — mlflow 3.13.0 broken on Python 3.12 (4 notebooks) + +`mlflow 3.13.0` (installed in the image) fails on Python 3.12 with: +``` +ImportError: attempted relative import beyond top-level package +``` +The error originates inside `mlflow.utils.uv_utils` which performs a relative +`from .. import zipp` that is invalid at top-level scope in Python 3.12. + +| Notebook | Where mlflow is used | +|---|---| +| `3d_segmentation/unet_segmentation_3d_ignite.ipynb` | `MLFlowHandler` optional import | +| `auto3dseg/notebooks/auto3dseg_hello_world.ipynb` | bundled `train.py` imports mlflow | +| `auto3dseg/notebooks/ensemble_byoc.ipynb` | bundled `train.py` imports mlflow | +| `experiment_management/spleen_segmentation_mlflow.ipynb` | direct `import mlflow` | + +**Fix:** pin `mlflow<3.0` in the Dockerfile (or constraint file): +```dockerfile +RUN pip install "mlflow<3.0" +``` +Eric passes these notebooks — his environment likely has an older mlflow. + +#### Group R2 — Disk full `OSError: [Errno 28] No space left on device` (4 notebooks) + +During the first rerun the generation notebooks wrote ~3.7 GB to `tutorials/generation/`, +consuming enough space to cause `Errno 28` for subsequent data-downloading notebooks. + +| Notebook | What it tried to write | +|---|---| +| `deep_atlas/deep_atlas_tutorial.ipynb` | OASIS dataset (~2 GB) | +| `deployment/bentoml/mednist_classifier_bentoml.ipynb` | MedNIST dataset | +| `experiment_management/bundle_integrate_mlflow.ipynb` | Spleen bundle run artefacts (ran 221 min before failing) | +| `microscopy/multichannel_microscopy_classification.ipynb` | Pre-trained DenseNet169 weights | + +**This is a run-order artifact, not a persistent issue on this host.** The root +filesystem has 39 GB free and the 3.7 GB generation datasets are now cached. On +subsequent runs with the generation data already present these notebooks complete +normally — confirmed by rerun on 2026-06-11 (`runner_output_rerun_r2r7r8.logs`). +Note: `bundle_integrate_mlflow` also requires the mlflow fix (Group R1) to be in +the rebuilt image before it can complete. + +#### Group R3 — transformers 5.10.2 + PyTorch nv25.03 incompatibility (1 notebook) + +`transformers 5.10.2` references `torch.float8_e8m0fnu` which does not exist in +PyTorch 2.7.0a0+nv25.03, causing import of `PreTrainedModel` to fail. + +| Notebook | Error | +|---|---| +| `hugging_face/hugging_face_pipeline_for_monai.ipynb` | `ModuleNotFoundError: Could not import module 'PreTrainedModel'` (caused by `AttributeError: module 'torch' has no attribute 'float8_e8m0fnu'`) | + +**Fix:** Pin `transformers<5.0` in constraint file, or wait for nv25 PyTorch update. +Eric's PyTorch 2.12+cu130 supports `float8_e8m0fnu`; ours does not. + +#### Group R4 — Missing MONAI module (1 notebook) + +| Notebook | Error | +|---|---| +| `2d_regression/image_restoration.ipynb` | `ModuleNotFoundError: No module named 'monai.networks.nets.restormer'` | + +`Restormer` is not yet present in our MONAI dev branch (19cab577). Eric is on +eccefc57 (+143 commits) which may already include it, or the notebook needs updating. + +**Fix:** Cherry-pick the Restormer commit into the dev branch, or add the +notebook to `skip_run_papermill` until the class is merged. + +#### Group R5 — MONAI `bundle.load` API change (1 notebook) + +| Notebook | Error | +|---|---| +| `computer_assisted_intervention/endoscopic_inbody_classification.ipynb` | `AttributeError: 'collections.OrderedDict' object has no attribute 'train'` | + +`monai.bundle.load(name="endoscopic_inbody_classification")` returns an +`OrderedDict` (the raw state dict) in our branch but the notebook calls +`.train()` on it expecting a full `nn.Module`. + +**Fix (notebook):** Replace +```python +pretrained_model = monai.bundle.load(name="endoscopic_inbody_classification", bundle_dir="./") +pretrained_model.train() +``` +with +```python +bundle = monai.bundle.load(name="endoscopic_inbody_classification", bundle_dir="./", + return_state_dict=False) # or use BundleWorkflow +``` +**Fix (MONAI):** Verify whether `bundle.load` is supposed to return a model or a +state dict and align the API with the notebook expectation. + +#### Group R6 — Missing `aim` package (1 notebook) + +| Notebook | Error | +|---|---| +| `experiment_management/spleen_segmentation_aim.ipynb` | `ModuleNotFoundError: No module named 'aim'` | + +**Fix:** Add `aim` to `requirements-dev.txt` and reinstall in the image. + +#### Group R7 — pytorch-lightning → mlflow import chain failure (1 notebook) + +| Notebook | Error | +|---|---| +| `bundle/05_spleen_segmentation_lightning.ipynb` | First run: `ContentTooShortError` (89 min download truncated). Rerun with `--ulimit nofile=65536:65536`: download succeeds in 6 min but training cell fails with `OptionalImportError: from scripts.main import train (No module named 'pytorch_lightning')`. | + +**Root cause chain:** +1. Notebook installs `pytorch-lightning~=2.0.0` → installs 2.0.9 +2. `pytorch_lightning` 2.0.x eagerly imports `mlflow` at module level (via `pytorch_lightning.loggers.mlflow`) +3. mlflow 3.13.0 fails to initialize under Python 3.12 (same R1 root cause) +4. Result: `import pytorch_lightning` fails in the `%%bash` training subprocess + +**Fix:** Change `!pip install -q pytorch-lightning~=2.0.0` → `!pip install -q "pytorch-lightning>=2.1"`. +pytorch-lightning ≥ 2.1 uses lazy mlflow imports; tested with 2.6.5, training and evaluation pass. +The original download truncation (ContentTooShortError) was due to fd limits (R8) and is also resolved with `--ulimit nofile=65536:65536`. + +#### Group R8 — Socket resource exhaustion (1 notebook) + +| Notebook | Error | +|---|---| +| `modules/public_datasets.ipynb` | `RuntimeError: received 0 items of ancdata` | + +PyTorch DataLoader failed to pass file descriptors through Unix sockets between +the main process and worker processes (ancillary data = file-descriptor passing). +This happens when the per-process open-file-descriptor limit is too low +(Docker default: 1024; DataLoader workers need ~65 k). + +**Fix:** Add `--ulimit nofile=65536:65536` to the docker run command (already +reflected in the working command above). Confirmed fixed in rerun +`runner_output_rerun_r2r7r8.logs`. + +#### Group R9 — MissingKeyword (1 notebook) + +| Notebook | Error | +|---|---| +| `auto3dseg/notebooks/msd_crossval_datalist_generator.ipynb` | `max_epochs` not found; not in exemption list | + +Known issue (documented in Category 2 above). Add to `doesnt_contain_max_epochs`. + +--- + +## Recommended Next Steps + +1. **Pin `mlflow<3.0`** in the Dockerfile — fixes 4 notebooks (Group R1). ✓ DONE (PR #8912) +2. **R2 is a run-order artifact** — not applicable on this host (39 GB free, data cached). +3. **`--ulimit nofile=65536:65536`** added to working docker run command — fixes R8. ✓ DONE +4. **PEP8 autofix** — all three notebooks autofixed with `runner.sh --autofix`. ✓ DONE +5. **Fix MissingKeyword** — `msd_crossval_datalist_generator.ipynb` added to exemption list. ✓ DONE (PR #2065) +6. **Pin `transformers<5.0`** — fixes R3. ✓ DONE (PR #8912) +7. **Add `aim`** to `requirements-dev.txt` — fixes R6. ✓ DONE (PR #8912) +8. **Fix `bundle.load` API** (R5) — `return_state_dict=False` added to notebook. ✓ DONE (PR #2065) +9. **Skip `image_restoration.ipynb`** until Restormer is merged (R4). ✓ DONE (PR #2065) +10. **Fix R7** — `pytorch-lightning>=2.1` pin in `bundle/05_spleen_segmentation_lightning.ipynb`. ✓ DONE (PR #2065) + +### Priority order + +| Priority | Action | Impact | Status | +|---|---|---|---| +| High | Pin `mlflow<3.0` (Dockerfile rebuild) | +4 passes | ✓ Done (PR #8912) | +| High | R2 disk-full | +4 passes | ✓ Not an issue (run-order artifact) | +| Medium | `--ulimit nofile=65536:65536` in docker run | +1 pass | ✓ Done | +| Medium | Add `msd_crossval_datalist_generator` to exemption | +1 pass | ✓ Done (PR #2065) | +| Medium | Pin `transformers<5.0` | +1 pass | ✓ Done (PR #8912) | +| Low | Add `aim` to requirements | +1 pass | ✓ Done (PR #8912) | +| Low | Fix `bundle.load` in endoscopic notebook | +1 pass | ✓ Done (PR #2065) | +| Low | PEP8 autofix (3 notebooks) | 0 fails eliminated | ✓ Done | +| Low | Skip `image_restoration.ipynb` (Restormer missing) | +1 pass | ✓ Done (PR #2065) | +| Low | Fix Spleen Lightning notebook (`pytorch-lightning>=2.1`) | +1 pass | ✓ Done (PR #2065) | + +--- + +## Fixes Applied (2026-06-11) + +Changes committed to bring Docker run to parity with Eric's run: + +### MONAI repo (`/data/rgd/MONAI`) + +| File | Change | +|---|---| +| `Dockerfile` | Base image: `24.10-py3` → `25.03-py3`; rebuild pip constraint file (keep `numpy==1.26.4`, add `setuptools<71`); install `papermill jupytext autopep8 autoflake ipywidgets`; pin `mlflow<3.0`, `transformers<5.0`; add `aim`, `lightning>=2.0` | +| `requirements-dev.txt` | Pin `transformers<5.0`; pin `mlflow<3.0`; add `aim`; add `lightning>=2.0`; remove `python_version<=3.10` caps from `cucim`, `onnxruntime`, `transformers` | + +### Tutorials repo (`/data/rgd/tutorials`) + +| File | Change | +|---|---| +| `runner.sh` | Add `msd_crossval_datalist_generator.ipynb` and `hovernet_infer_compare.ipynb` to `doesnt_contain_max_epochs`; add `image_restoration.ipynb` to `skip_run_papermill` | +| `computer_assisted_intervention/endoscopic_inbody_classification.ipynb` | Pass `return_state_dict=False` to `monai.bundle.load` so it returns `nn.Module` instead of `OrderedDict` | +| `bundle/05_spleen_segmentation_lightning.ipynb` | Change `pytorch-lightning~=2.0.0` → `pytorch-lightning>=2.1` to avoid mlflow eager-import failure (R7) | + +### Still pending (require environment changes or separate PRs) + +| Issue | Action needed | +|---|---| +| Disk space (R2) | Free `/data` disk or bind-mount scratch volume; `deep_atlas` (~2 GB), `deployment/bentoml`, `experiment_management/bundle_integrate_mlflow`, `microscopy` notebooks | +| Socket FD limit (R8) | Add `--ulimit nofile=65536:65536` to docker run invocation | +| pytorch-lightning/mlflow import chain (R7) | ✓ Fixed: `pytorch-lightning>=2.1` in notebook (PR #2065) | +| PEP8 in 3 notebooks | Run `bash runner.sh --autofix` for `surgtoolloc/preprocess_detect_scene_and_split_fold.ipynb`, `deep_atlas/deep_atlas_tutorial.ipynb`, `modules/interpretability/class_lung_lesion.ipynb` | +| Restormer in MONAI dev | Cherry-pick Restormer network class commit; remove from skip list once merged | From f3f49ff0a2a7514e5e523ee4c97663561d06fc82 Mon Sep 17 00:00:00 2001 From: "R. Garcia-Dias" Date: Thu, 11 Jun 2026 13:36:46 +0100 Subject: [PATCH 05/10] fix: revert return_state_dict change; update R5/R8 diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revert return_state_dict=False from endoscopic_inbody_classification.ipynb: the upstream MONAI dev (≥1.5) removed the deprecated param entirely and load() now returns nn.Module by default. The extra kwarg caused a TypeError in CI. The original notebook is correct for MONAI ≥1.5. - Update diagnose_1_6_release.md: - R5: clarify it is a local-only issue (our MONAI @ 19cab577 still has the deprecated return_state_dict=True default; upstream dev does not) - R8: mark confirmed fixed — isolated run with --ulimit nofile=65536:65536 completed all 39 cells in 3m1s with no ancdata error (2026-06-11) Signed-off-by: R. Garcia-Dias --- .../endoscopic_inbody_classification.ipynb | 4 +- diagnose_1_6_release.md | 53 ++++++++++--------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/computer_assisted_intervention/endoscopic_inbody_classification.ipynb b/computer_assisted_intervention/endoscopic_inbody_classification.ipynb index 95d96f691..6a5999046 100644 --- a/computer_assisted_intervention/endoscopic_inbody_classification.ipynb +++ b/computer_assisted_intervention/endoscopic_inbody_classification.ipynb @@ -347,7 +347,7 @@ "id": "4784fe9e", "metadata": {}, "outputs": [], - "source": "max_epochs = 5\n\n# return_state_dict=False returns the nn.Module rather than a raw OrderedDict (MONAI 1.6 API)\npretrained_model = monai.bundle.load(name=\"endoscopic_inbody_classification\", bundle_dir=\"./\", return_state_dict=False)\n\npretrained_model.train()\nlosses = []\n\nfor _ in trange(max_epochs):\n epoch_loss = 0\n for data in train_dataloader:\n inputs, labels = data[\"image\"].to(device), data[\"label\"].to(device)\n optimizer.zero_grad()\n predictions = pretrained_model(inputs)\n loss_iter = loss(predictions, labels)\n loss_iter.backward()\n optimizer.step()\n epoch_loss += loss_iter.item()\n losses.append(epoch_loss / len(train_dataloader))\n\nfig, ax = plt.subplots(1, 1, figsize=(6, 6), facecolor=\"white\")\nax.set_xlabel(\"Epoch\")\nepochs = list(range(len(losses)))\nax.plot(epochs, losses, label=\"loss\")\nplt.legend()\nplt.show()" + "source": "max_epochs = 5\n\npretrained_model = monai.bundle.load(name=\"endoscopic_inbody_classification\", bundle_dir=\"./\")\n\npretrained_model.train()\nlosses = []\n\nfor _ in trange(max_epochs):\n epoch_loss = 0\n for data in train_dataloader:\n inputs, labels = data[\"image\"].to(device), data[\"label\"].to(device)\n optimizer.zero_grad()\n predictions = pretrained_model(inputs)\n loss_iter = loss(predictions, labels)\n loss_iter.backward()\n optimizer.step()\n epoch_loss += loss_iter.item()\n losses.append(epoch_loss / len(train_dataloader))\n\nfig, ax = plt.subplots(1, 1, figsize=(6, 6), facecolor=\"white\")\nax.set_xlabel(\"Epoch\")\nepochs = list(range(len(losses)))\nax.plot(epochs, losses, label=\"loss\")\nplt.legend()\nplt.show()" }, { "cell_type": "markdown", @@ -407,4 +407,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/diagnose_1_6_release.md b/diagnose_1_6_release.md index 261a37d5b..576d60ebf 100644 --- a/diagnose_1_6_release.md +++ b/diagnose_1_6_release.md @@ -318,28 +318,25 @@ eccefc57 (+143 commits) which may already include it, or the notebook needs upda **Fix:** Cherry-pick the Restormer commit into the dev branch, or add the notebook to `skip_run_papermill` until the class is merged. -#### Group R5 — MONAI `bundle.load` API change (1 notebook) +#### Group R5 — MONAI `bundle.load` API — local-only issue (1 notebook) -| Notebook | Error | +| Notebook | Error (local MONAI dev @ 19cab577 only) | |---|---| | `computer_assisted_intervention/endoscopic_inbody_classification.ipynb` | `AttributeError: 'collections.OrderedDict' object has no attribute 'train'` | -`monai.bundle.load(name="endoscopic_inbody_classification")` returns an -`OrderedDict` (the raw state dict) in our branch but the notebook calls -`.train()` on it expecting a full `nn.Module`. +`monai.bundle.load()` in our local MONAI dev branch (19cab577) has +`@deprecated_arg("return_state_dict", since="1.2", removed="1.5")` with +`return_state_dict=True` still active as the default, so it returns an +`OrderedDict` instead of an `nn.Module`. -**Fix (notebook):** Replace -```python -pretrained_model = monai.bundle.load(name="endoscopic_inbody_classification", bundle_dir="./") -pretrained_model.train() -``` -with -```python -bundle = monai.bundle.load(name="endoscopic_inbody_classification", bundle_dir="./", - return_state_dict=False) # or use BundleWorkflow -``` -**Fix (MONAI):** Verify whether `bundle.load` is supposed to return a model or a -state dict and align the API with the notebook expectation. +**This is a local-environment-only issue.** The upstream MONAI dev branch +(eccefc57, used by CI and the upstream Docker image) removed the +`return_state_dict` parameter in MONAI 1.5, and `load()` now returns `nn.Module` +by default. The notebook is correct as-is for any MONAI ≥ 1.5. + +**No notebook fix needed.** The `return_state_dict=False` workaround that was +previously applied broke CI because the upstream MONAI does not accept that +parameter at all (`TypeError: unexpected keyword argument`). It has been reverted. #### Group R6 — Missing `aim` package (1 notebook) @@ -377,8 +374,14 @@ This happens when the per-process open-file-descriptor limit is too low (Docker default: 1024; DataLoader workers need ~65 k). **Fix:** Add `--ulimit nofile=65536:65536` to the docker run command (already -reflected in the working command above). Confirmed fixed in rerun -`runner_output_rerun_r2r7r8.logs`. +reflected in the working command above). + +**Rerun note:** The targeted rerun (`runner_output_rerun_r2r7r8.logs`) ran `public_datasets` in +the same container as `deep_atlas`. That notebook pip-installs packages which upgraded `urllib3` +to 2.x; `papermill` then failed to import immediately (`ModuleNotFoundError: No module named +'urllib3.packages.six.moves'`) — a run-order contamination, **not** the ancdata fix being tested. +**Confirmed fixed** in isolated run (container with `--ulimit nofile=65536:65536`, no shared +container): all 39 cells passed, `real 3m1s`. No `ancdata` error observed. #### Group R9 — MissingKeyword (1 notebook) @@ -394,12 +397,12 @@ Known issue (documented in Category 2 above). Add to `doesnt_contain_max_epochs` 1. **Pin `mlflow<3.0`** in the Dockerfile — fixes 4 notebooks (Group R1). ✓ DONE (PR #8912) 2. **R2 is a run-order artifact** — not applicable on this host (39 GB free, data cached). -3. **`--ulimit nofile=65536:65536`** added to working docker run command — fixes R8. ✓ DONE +3. **`--ulimit nofile=65536:65536`** added to working docker run command — fixes R8. ✓ DONE (confirmed clean run 3m1s) 4. **PEP8 autofix** — all three notebooks autofixed with `runner.sh --autofix`. ✓ DONE 5. **Fix MissingKeyword** — `msd_crossval_datalist_generator.ipynb` added to exemption list. ✓ DONE (PR #2065) 6. **Pin `transformers<5.0`** — fixes R3. ✓ DONE (PR #8912) 7. **Add `aim`** to `requirements-dev.txt` — fixes R6. ✓ DONE (PR #8912) -8. **Fix `bundle.load` API** (R5) — `return_state_dict=False` added to notebook. ✓ DONE (PR #2065) +8. **R5 is local-only** — no notebook fix needed; upstream MONAI ≥ 1.5 removed the deprecated param and `load()` returns `nn.Module` directly. ✓ CONFIRMED (reverted wrong fix) 9. **Skip `image_restoration.ipynb`** until Restormer is merged (R4). ✓ DONE (PR #2065) 10. **Fix R7** — `pytorch-lightning>=2.1` pin in `bundle/05_spleen_segmentation_lightning.ipynb`. ✓ DONE (PR #2065) @@ -409,11 +412,11 @@ Known issue (documented in Category 2 above). Add to `doesnt_contain_max_epochs` |---|---|---|---| | High | Pin `mlflow<3.0` (Dockerfile rebuild) | +4 passes | ✓ Done (PR #8912) | | High | R2 disk-full | +4 passes | ✓ Not an issue (run-order artifact) | -| Medium | `--ulimit nofile=65536:65536` in docker run | +1 pass | ✓ Done | +| Medium | `--ulimit nofile=65536:65536` in docker run | +1 pass | ✓ Done (confirmed, 3m1s clean run) | | Medium | Add `msd_crossval_datalist_generator` to exemption | +1 pass | ✓ Done (PR #2065) | | Medium | Pin `transformers<5.0` | +1 pass | ✓ Done (PR #8912) | | Low | Add `aim` to requirements | +1 pass | ✓ Done (PR #8912) | -| Low | Fix `bundle.load` in endoscopic notebook | +1 pass | ✓ Done (PR #2065) | +| Low | R5 `bundle.load` API — local-only, no fix needed | +1 pass (upstream) | ✓ Confirmed (reverted bad fix) | | Low | PEP8 autofix (3 notebooks) | 0 fails eliminated | ✓ Done | | Low | Skip `image_restoration.ipynb` (Restormer missing) | +1 pass | ✓ Done (PR #2065) | | Low | Fix Spleen Lightning notebook (`pytorch-lightning>=2.1`) | +1 pass | ✓ Done (PR #2065) | @@ -436,7 +439,7 @@ Changes committed to bring Docker run to parity with Eric's run: | File | Change | |---|---| | `runner.sh` | Add `msd_crossval_datalist_generator.ipynb` and `hovernet_infer_compare.ipynb` to `doesnt_contain_max_epochs`; add `image_restoration.ipynb` to `skip_run_papermill` | -| `computer_assisted_intervention/endoscopic_inbody_classification.ipynb` | Pass `return_state_dict=False` to `monai.bundle.load` so it returns `nn.Module` instead of `OrderedDict` | +| `computer_assisted_intervention/endoscopic_inbody_classification.ipynb` | Reverted `return_state_dict=False` — upstream MONAI ≥1.5 already returns `nn.Module` by default; the extra kwarg caused `TypeError` in CI | | `bundle/05_spleen_segmentation_lightning.ipynb` | Change `pytorch-lightning~=2.0.0` → `pytorch-lightning>=2.1` to avoid mlflow eager-import failure (R7) | ### Still pending (require environment changes or separate PRs) @@ -444,7 +447,7 @@ Changes committed to bring Docker run to parity with Eric's run: | Issue | Action needed | |---|---| | Disk space (R2) | Free `/data` disk or bind-mount scratch volume; `deep_atlas` (~2 GB), `deployment/bentoml`, `experiment_management/bundle_integrate_mlflow`, `microscopy` notebooks | -| Socket FD limit (R8) | Add `--ulimit nofile=65536:65536` to docker run invocation | +| Socket FD limit (R8) | ✓ Confirmed fixed — `--ulimit nofile=65536:65536` resolves ancdata error; isolated run completed in 3m1s (2026-06-11) | | pytorch-lightning/mlflow import chain (R7) | ✓ Fixed: `pytorch-lightning>=2.1` in notebook (PR #2065) | | PEP8 in 3 notebooks | Run `bash runner.sh --autofix` for `surgtoolloc/preprocess_detect_scene_and_split_fold.ipynb`, `deep_atlas/deep_atlas_tutorial.ipynb`, `modules/interpretability/class_lung_lesion.ipynb` | | Restormer in MONAI dev | Cherry-pick Restormer network class commit; remove from skip list once merged | From a581217a8aa01701f42cd6ac03868b893b1a469b Mon Sep 17 00:00:00 2001 From: "R. Garcia-Dias" Date: Thu, 11 Jun 2026 14:10:35 +0100 Subject: [PATCH 06/10] style: add trailing newline to endoscopic_inbody_classification.ipynb Signed-off-by: R. Garcia-Dias --- .../endoscopic_inbody_classification.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/computer_assisted_intervention/endoscopic_inbody_classification.ipynb b/computer_assisted_intervention/endoscopic_inbody_classification.ipynb index 6a5999046..1b9f071e7 100644 --- a/computer_assisted_intervention/endoscopic_inbody_classification.ipynb +++ b/computer_assisted_intervention/endoscopic_inbody_classification.ipynb @@ -407,4 +407,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} From c62cc063f9a681537377b5f5b1602bca7c54d824 Mon Sep 17 00:00:00 2001 From: "R. Garcia-Dias" Date: Thu, 11 Jun 2026 14:47:31 +0100 Subject: [PATCH 07/10] fix: skip 05_spleen_segmentation_lightning in CPU CI The notebook calls .to(device) where device is CUDA unconditionally; no CPU fallback exists. Fails with AssertionError: Torch not compiled with CUDA enabled on the CPU-only GitHub Actions runner. Signed-off-by: R. Garcia-Dias --- runner.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/runner.sh b/runner.sh index 0474f124a..92104ef4d 100755 --- a/runner.sh +++ b/runner.sh @@ -138,6 +138,7 @@ skip_run_papermill=("${skip_run_papermill[@]}" .*finetune_vista3d_for_hugging_fa skip_run_papermill=("${skip_run_papermill[@]}" .*TCIA_PROSTATEx_Prostate_MRI_Anatomy_Model.ipynb*) # https://github.com/Project-MONAI/tutorials/issues/2029 skip_run_papermill=("${skip_run_papermill[@]}" .*maisi_inference_tutorial.ipynb*) skip_run_papermill=("${skip_run_papermill[@]}" .*image_restoration.ipynb*) # monai.networks.nets.restormer not yet in dev branch +skip_run_papermill=("${skip_run_papermill[@]}" .*05_spleen_segmentation_lightning*) # requires GPU; hardcoded .to("cuda") with no CPU fallback # output formatting separator="" From 3d198d814d3857620a13dcd95069411bd05a125a Mon Sep 17 00:00:00 2001 From: "R. Garcia-Dias" Date: Thu, 11 Jun 2026 15:05:58 +0100 Subject: [PATCH 08/10] fix: skip deep_atlas_tutorial in CPU CI deep_atlas_tutorial.ipynb hardcodes device = torch.device("cuda:0") with no CPU fallback; fails with AssertionError: Torch not compiled with CUDA enabled on the CPU-only GitHub Actions runner. Signed-off-by: R. Garcia-Dias --- runner.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/runner.sh b/runner.sh index 92104ef4d..ae4f6d85b 100755 --- a/runner.sh +++ b/runner.sh @@ -139,6 +139,7 @@ skip_run_papermill=("${skip_run_papermill[@]}" .*TCIA_PROSTATEx_Prostate_MRI_Ana skip_run_papermill=("${skip_run_papermill[@]}" .*maisi_inference_tutorial.ipynb*) skip_run_papermill=("${skip_run_papermill[@]}" .*image_restoration.ipynb*) # monai.networks.nets.restormer not yet in dev branch skip_run_papermill=("${skip_run_papermill[@]}" .*05_spleen_segmentation_lightning*) # requires GPU; hardcoded .to("cuda") with no CPU fallback +skip_run_papermill=("${skip_run_papermill[@]}" .*deep_atlas_tutorial*) # requires GPU; device hardcoded to "cuda:0" # output formatting separator="" From d38d10149e5e7f1c81164d5556e9f8b20cd1e6be Mon Sep 17 00:00:00 2001 From: "R. Garcia-Dias" Date: Thu, 11 Jun 2026 15:35:15 +0100 Subject: [PATCH 09/10] fix: remove non-existent hovernet_infer_compare from doesnt_contain_max_epochs; fix stale pending table - runner.sh: hovernet_infer_compare.ipynb does not exist in the repo; the exemption was a dead entry that could never match. Removed. - diagnose_1_6_release.md: PEP8 row removed from the 'Still pending' table (autofix was already applied and marked done at line 401). Also updated runner.sh change summary to reflect the actual skip list entries (deep_atlas_tutorial, 05_spleen_segmentation_lightning). Signed-off-by: R. Garcia-Dias --- diagnose_1_6_release.md | 3 +-- runner.sh | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/diagnose_1_6_release.md b/diagnose_1_6_release.md index 576d60ebf..79d0e0c4c 100644 --- a/diagnose_1_6_release.md +++ b/diagnose_1_6_release.md @@ -438,7 +438,7 @@ Changes committed to bring Docker run to parity with Eric's run: | File | Change | |---|---| -| `runner.sh` | Add `msd_crossval_datalist_generator.ipynb` and `hovernet_infer_compare.ipynb` to `doesnt_contain_max_epochs`; add `image_restoration.ipynb` to `skip_run_papermill` | +| `runner.sh` | Add `msd_crossval_datalist_generator.ipynb` to `doesnt_contain_max_epochs`; add `image_restoration.ipynb`, `05_spleen_segmentation_lightning.ipynb`, `deep_atlas_tutorial.ipynb` to `skip_run_papermill` | | `computer_assisted_intervention/endoscopic_inbody_classification.ipynb` | Reverted `return_state_dict=False` — upstream MONAI ≥1.5 already returns `nn.Module` by default; the extra kwarg caused `TypeError` in CI | | `bundle/05_spleen_segmentation_lightning.ipynb` | Change `pytorch-lightning~=2.0.0` → `pytorch-lightning>=2.1` to avoid mlflow eager-import failure (R7) | @@ -449,5 +449,4 @@ Changes committed to bring Docker run to parity with Eric's run: | Disk space (R2) | Free `/data` disk or bind-mount scratch volume; `deep_atlas` (~2 GB), `deployment/bentoml`, `experiment_management/bundle_integrate_mlflow`, `microscopy` notebooks | | Socket FD limit (R8) | ✓ Confirmed fixed — `--ulimit nofile=65536:65536` resolves ancdata error; isolated run completed in 3m1s (2026-06-11) | | pytorch-lightning/mlflow import chain (R7) | ✓ Fixed: `pytorch-lightning>=2.1` in notebook (PR #2065) | -| PEP8 in 3 notebooks | Run `bash runner.sh --autofix` for `surgtoolloc/preprocess_detect_scene_and_split_fold.ipynb`, `deep_atlas/deep_atlas_tutorial.ipynb`, `modules/interpretability/class_lung_lesion.ipynb` | | Restormer in MONAI dev | Cherry-pick Restormer network class commit; remove from skip list once merged | diff --git a/runner.sh b/runner.sh index ae4f6d85b..431cc9ecc 100755 --- a/runner.sh +++ b/runner.sh @@ -85,7 +85,6 @@ doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" realism_diversity_m doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" omniverse_integration.ipynb) doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" hugging_face_pipeline_for_monai.ipynb) doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" msd_crossval_datalist_generator.ipynb) # inference/datalist-only notebook, no training loop -doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" hovernet_infer_compare.ipynb) # inference-only notebook # Execution of the notebook in these folders / with the filename cannot be automated skip_run_papermill=() From 1a760d6b61c08a2f9d48414e64e1667ac5b32d5f Mon Sep 17 00:00:00 2001 From: "R. Garcia-Dias" Date: Tue, 16 Jun 2026 13:43:58 +0100 Subject: [PATCH 10/10] feat: add --jobs N parallel execution and --data-dir caching to runner.sh --jobs N (default 1, backwards-compatible): Runs N notebooks concurrently via background subshells + wait -n semaphore. Per-notebook stdout/stderr goes to an isolated temp log; logs are printed in original notebook order after all jobs finish. Recommended value for download-heavy runs: 4-8. Use --jobs 1 when notebooks do conflicting pip installs (e.g. GPU-training notebooks). --data-dir PATH: Exports MONAI_DATA_DIRECTORY=PATH before notebooks run. 109/117 runnable notebooks honour this env var and fall back to a temp dir when it is absent -- so without it every container run re-downloads all datasets. Setting it to a bind-mounted host path caches data permanently. Typical Docker invocation: docker run ... \ -v /host/monai_data:/data/monai_cache \ monai_1_6:latest \ bash -c "cd /opt/tutorials && bash runner.sh --jobs 4 --data-dir /data/monai_cache" Also respects MONAI_DATA_DIRECTORY already set in the environment (e.g. passed via docker run -e) without requiring --data-dir. Per-notebook logic extracted into _run_notebook() so both paths share one implementation; sequential path behaviour is unchanged. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: R. Garcia-Dias --- runner.sh | 186 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 142 insertions(+), 44 deletions(-) diff --git a/runner.sh b/runner.sh index 431cc9ecc..fc9c19ee8 100755 --- a/runner.sh +++ b/runner.sh @@ -164,6 +164,8 @@ autofix=false failfast=false pattern="" papermill_opt="" +jobs=1 +data_dir="" kernelspec="python3" @@ -173,7 +175,7 @@ NB_OUTPUT_LINE_CAP=100 function print_usage { echo "runner.sh [--no-run] [--no-checks] [--autofix] [-f/--failfast] [-p/--pattern ] [-h/--help]" - echo "[-v/--version] [--verbose]" + echo "[-v/--version] [--verbose] [-j/--jobs ] [--data-dir ]" echo "" echo "MONAI tutorials testing utilities. When running the notebooks, we first search for variables, such as" echo "\"max_epochs\" and set them to 1 to reduce testing time." @@ -184,12 +186,21 @@ function print_usage { echo " --autofix : autofix where possible" echo " --cell-standard : check guidelines standards such as ## setup environment cell blocks" echo " --copyright : check whether every source code and notebook has a copyright header" - echo " -f, --failfast : stop on first error" + echo " -f, --failfast : stop on first error (ignored when --jobs > 1)" echo " -p, --pattern : pattern of files to be run (added to \`find . -type f -name *.ipynb -and ! -wholename *.ipynb_checkpoints*\`)" echo " -h, --help : show this help message and exit" echo " -t, --test : shortcut to run a single notebook using pattern \`-and -wholename\`" echo " -v, --version : show MONAI and system version information and exit" - echo " --verbose : show papermill logs when testing the noteboobks" + echo " --verbose : show papermill logs when testing the notebooks" + echo " -j, --jobs N : run N notebooks in parallel (default: 1). Each notebook logs independently;" + echo " logs are printed in original order after all jobs finish." + echo " Note: parallel jobs share the same Python environment; use --jobs 1 when" + echo " notebooks do conflicting pip installs." + echo " --data-dir PATH : set MONAI_DATA_DIRECTORY to PATH before running notebooks. Notebooks that" + echo " respect this env var will persist downloads there instead of a temp dir." + echo " Tip: mount a host directory at this path in Docker to cache across runs:" + echo " docker run ... -v /host/data:/data -e MONAI_DATA_DIRECTORY=/data ..." + echo " or pass --data-dir /data to this script after the Docker bind-mount." echo "" echo "Examples:" echo "./runner.sh # run full tests (${green}recommended before making pull requests${noColor})." @@ -201,6 +212,8 @@ function print_usage { echo " # check filenames containing \"read\" or \"load\", but not if the" echo " whole path contains \"deepgrow\"." echo "./runner.sh --kernelspec \"kernel\" # Set the kernelspec value used to run notebooks, default is \"python3\"." + echo "./runner.sh -j 4 --data-dir /data/monai_cache" + echo " # run 4 notebooks in parallel; reuse cached downloads." echo "./runner.sh --no-checks --no-run --copyright echo " # test if all notebooks and scripts have the copyright header" echo "./runner.sh --no-checks --no-run --cell-standard @@ -251,6 +264,14 @@ do echo $pattern shift ;; + -j|--jobs) + jobs="$2" + shift + ;; + --data-dir) + data_dir="$2" + shift + ;; -k|--kernelspec) kernelspec="$2" shift @@ -433,6 +454,19 @@ fi base_path="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" cd "${base_path}" +# Export MONAI_DATA_DIRECTORY so notebooks persist downloads across runs. +# 109/117 runnable notebooks honour this env var (pattern: +# directory = os.environ.get("MONAI_DATA_DIRECTORY") +# root_dir = tempfile.mkdtemp() if directory is None else directory +# Without it each notebook re-downloads on every container run. +if [ -n "$data_dir" ]; then + mkdir -p "$data_dir" + export MONAI_DATA_DIRECTORY="$data_dir" + echo "Data cache: $data_dir (MONAI_DATA_DIRECTORY)" +elif [ -n "${MONAI_DATA_DIRECTORY:-}" ]; then + echo "Data cache: $MONAI_DATA_DIRECTORY (MONAI_DATA_DIRECTORY from environment)" +fi + function replace_text { oldString="${s}\s*=\s*[0-9]\+" newString="${s} = 1" @@ -485,24 +519,28 @@ fi ######################################################################## # # -# loop over files # +# per-notebook logic (used by both sequential and parallel paths) # # # ######################################################################## -for file in "${files[@]}"; do - current_test_successful=0 +# _run_notebook FILE RESULT_FILE +# Runs PEP8 checks and/or papermill for FILE. +# Writes 0 (pass) or 1 (fail) to RESULT_FILE. +# Must be called in a subshell so cwd changes are isolated. +function _run_notebook { + local file="$1" + local result_file="$2" + local current_test_successful=0 echo "${separator}${blue}Running $file${noColor}" - # Get to file's folder and get file contents + local path filename path="$(dirname "${file}")" filename="$(basename "${file}")" - cd ${base_path}/${path} + cd "${base_path}/${path}" - ######################################################################## - # # - # code checks # - # # - ######################################################################## + #################################################################### + # code checks # + #################################################################### if [ $doChecks = true ]; then if [ $autofix = true ]; then @@ -513,33 +551,26 @@ for file in "${files[@]}"; do --pipe "sed 's/ = list()/ = []/'" fi - # to check flake8, convert to python script, don't check - # magic cells, and don't check line length for comment - # lines (as this includes markdown), and then run flake8 echo Checking PEP8 compliance... jupytext "$filename" --opt custom_cell_magics="writefile" -w --to script -o - | \ sed 's/\(^\s*\)%/\1pass # %/' | \ sed 's/\(^#.*\)$/\1 # noqa: E501/' | \ flake8 - --show-source --extend-ignore=E203,N812,W503 --max-line-length 120 - success=$? - if [ ${success} -ne 0 ] - then + local success=$? + if [ ${success} -ne 0 ]; then print_error_msg "Try running with autofixes: ${green}--autofix${noColor}" - test_fail ${success} + current_test_successful=1 fi fi - ######################################################################## - # # - # run notebooks with papermill # - # # - ######################################################################## - if [ $doRun = true ]; then - - skipRun=false + #################################################################### + # run notebook with papermill # + #################################################################### + if [ $doRun = true ] && [ $current_test_successful -eq 0 ]; then + local skipRun=false for skip_pattern in "${skip_run_papermill[@]}"; do - if [[ $file =~ $skip_pattern ]]; then + if [[ $file =~ $skip_pattern ]]; then echo "Skip Pattern Match" skipRun=true break @@ -548,45 +579,112 @@ for file in "${files[@]}"; do if [ $skipRun = true ]; then echo "Skipping" - continue + echo "$current_test_successful" > "$result_file" + return fi echo Running notebook... + local notebook notebook=$(cat "$filename") - # if compulsory keyword, max_epochs, missing... if [[ ! "$notebook" =~ "max_epochs" ]]; then - # and notebook isn't in list of those expected to not have that keyword... - should_contain_max_epochs=true + local should_contain_max_epochs=true for e in "${doesnt_contain_max_epochs[@]}"; do [[ "$e" == "$filename" ]] && should_contain_max_epochs=false && break done - # then error if [[ $should_contain_max_epochs == true ]]; then print_error_msg "Couldn't find the keyword \"max_epochs\", and the notebook wasn't on the list of expected exemptions (\"doesnt_contain_max_epochs\")." - test_fail 1 + current_test_successful=1 + echo "$current_test_successful" > "$result_file" + return fi fi - # Set some variables to 1 to speed up proceedings - strings_to_replace=(max_epochs val_interval disc_train_interval disc_train_steps num_batches_for_histogram) + local strings_to_replace=(max_epochs val_interval disc_train_interval disc_train_steps num_batches_for_histogram) for s in "${strings_to_replace[@]}"; do replace_text done python -c 'import monai; monai.config.print_config()' + local cmd cmd=$(echo "papermill ${papermill_opt} --progress-bar --log-output -k ${kernelspec}") echo "$cmd" + local out time out=$(echo "$notebook" | eval "$cmd") - success=$? + local success=$? if [[ ${success} -ne 0 || "$out" =~ "\"status\": \"failed\"" ]]; then - test_fail ${success} + current_test_successful=1 fi fi - num_tested=$((num_tested + 1)) - if [[ ${current_test_successful} -eq 0 ]]; then - num_successful_tests=$((num_successful_tests + 1)) - fi -done + echo "$current_test_successful" > "$result_file" +} + +######################################################################## +# # +# loop over files — sequential (jobs=1) or parallel (jobs>1) # +# # +######################################################################## +if [ "$jobs" -le 1 ]; then + # ---------------------------------------------------------------- # + # Sequential path — original behaviour, unchanged # + # ---------------------------------------------------------------- # + for file in "${files[@]}"; do + current_test_successful=0 + _result_file=$(mktemp) + + ( trap - EXIT; _run_notebook "$file" "$_result_file" ) + + current_test_successful=$(cat "$_result_file" 2>/dev/null || echo 1) + rm -f "$_result_file" + + num_tested=$((num_tested + 1)) + if [[ ${current_test_successful} -eq 0 ]]; then + num_successful_tests=$((num_successful_tests + 1)) + elif [ $failfast = true ]; then + finish + fi + done + +else + # ---------------------------------------------------------------- # + # Parallel path — N notebooks run concurrently # + # ---------------------------------------------------------------- # + echo "Running ${#files[@]} notebooks with --jobs $jobs" + _work_dir=$(mktemp -d) + + for file in "${files[@]}"; do + # Throttle: wait until a slot is free + while [ "$(jobs -rp | wc -l)" -ge "$jobs" ]; do + wait -n 2>/dev/null || sleep 0.2 + done + + _slug=$(printf '%s' "$file" | tr '/.' '--') + _log="${_work_dir}/${_slug}.log" + _result="${_work_dir}/${_slug}.result" + + ( + trap - EXIT + set +e + _run_notebook "$file" "$_result" + ) > "$_log" 2>&1 & + done + + wait # wait for all remaining background jobs + + # Print logs in original notebook order; collect pass/fail counts + for file in "${files[@]}"; do + _slug=$(printf '%s' "$file" | tr '/.' '--') + _log="${_work_dir}/${_slug}.log" + _result="${_work_dir}/${_slug}.result" + + cat "$_log" + num_tested=$((num_tested + 1)) + if [ "$(cat "$_result" 2>/dev/null)" = "0" ]; then + num_successful_tests=$((num_successful_tests + 1)) + fi + done + + rm -rf "$_work_dir" +fi