Skip to content

Commit 81dcf0f

Browse files
garciadiasclaudepre-commit-ci[bot]
authored
fix: update runner.sh and endoscopic notebook for MONAI 1.6 tutorial runner compatibility (#2065)
### Description Fixes tutorial runner failures observed during MONAI 1.6 release testing (Docker image, Python 3.12, RTX 5090). Full diagnostics in [this comment](#2065 (comment)). > **Depends on [Project-MONAI/MONAI#8912](Project-MONAI/MONAI#8912 > That PR updates the MONAI Docker image (`Dockerfile` + `requirements-dev.txt`) with the environment fixes that several tutorials here rely on: it pins `mlflow<3.0` (fixes R1 — 4 mlflow-using notebooks that fail on Python 3.12), pins `transformers<5.0` (fixes R3 — `torch.float8_e8m0fnu` missing in the base PyTorch build), adds `aim` (fixes R6 — spleen segmentation AIM tutorial), and adds `lightning>=2.0` as a declared dependency. Without #8912 the Docker image still installs broken versions of those packages, so those tutorial failures persist regardless of the changes in this PR. ### Failures addressed | ID | Root cause | Affected notebook | Fix applied | Needs #8912 | |---|---|---|---|---| | R1 | `mlflow 3.x` fails on Python 3.12 (`from .. import zipp` relative import) | 4 mlflow-using notebooks | Pin `mlflow<3.0` in `requirements-dev.txt` | ✅ Fixed in #8912 | | R3 | `transformers 5.x` references `torch.float8_e8m0fnu` absent in the base PyTorch build | `2d_regression/image_restoration.ipynb` (transitively) | Pin `transformers<5.0` in `requirements-dev.txt` | ✅ Fixed in #8912 | | R4 | `monai.networks.nets.restormer` not present in this branch | `2d_regression/image_restoration.ipynb` | Added to `skip_run_papermill` in `runner.sh` | — | | R5 | `monai.bundle.load()` returns `OrderedDict` in older local branch — local-only issue; upstream MONAI ≥1.5 already returns `nn.Module` by default | `computer_assisted_intervention/endoscopic_inbody_classification.ipynb` | Reverted erroneous `return_state_dict=False` kwarg; original notebook is correct | — | | R6 | `aim` package not installed in the Docker image | `modules/spleen_segmentation_aim.ipynb` | Add `aim` to `requirements-dev.txt` | ✅ Fixed in #8912 | | R7 | `pytorch-lightning~=2.0.0` (resolves to 2.0.9) eagerly imports `mlflow` at module level; `mlflow 3.x` fails on Python 3.12 with a relative-import error | `bundle/05_spleen_segmentation_lightning.ipynb` | Pinned `pytorch-lightning>=2.1` in notebook (removes the eager mlflow import; tested with 2.6.5) | Partial — #8912 pins `mlflow<3.0` as a safety net; this PR removes the fragile import dependency entirely | | R9 | Runner requires all training notebooks to declare `max_epochs`; two notebooks have none | `bundle/msd_crossval_datalist_generator.ipynb`, `pathology/hovernet_infer_compare.ipynb` | Added both to `doesnt_contain_max_epochs` exemption list in `runner.sh` | — | | CI | `deep_atlas_tutorial.ipynb` and `05_spleen_segmentation_lightning.ipynb` hardcode `device = torch.device("cuda:0")` / `"cuda"` with no CPU fallback; fail on the CPU-only GitHub Actions runner | `deep_atlas/deep_atlas_tutorial.ipynb`, `bundle/05_spleen_segmentation_lightning.ipynb` | Added both to `skip_run_papermill` in `runner.sh` | — | | PEP8 | E225/E231 whitespace violations flagged by pre-commit | `preprocess_detect_scene_and_split_fold.ipynb`, `deep_atlas_tutorial.ipynb`, `class_lung_lesion.ipynb` | Applied `autopep8` autofix (no functional changes) | — | ### Checks - [x] Avoid including large-size files in the PR. - [x] Clean up long text outputs from code cells in the notebook. - [x] For security purposes, please check the contents and remove any sensitive info such as user names and private key. - [x] Ensure (1) hyperlinks and markdown anchors are working (2) use relative paths for tutorial repo files (3) put figure and graphs in the `./figure` folder - [ ] Notebook runs automatically `./runner.sh -t <path to .ipynb file>` — GPU-dependent notebooks (`05_spleen_segmentation_lightning`, `deep_atlas_tutorial`) require a CUDA device; they are skipped in the CPU CI runner and verified manually on a local GPU node. --------- Signed-off-by: R. Garcia-Dias <rafaelagd@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 7bedfd0 commit 81dcf0f

5 files changed

Lines changed: 154 additions & 52 deletions

File tree

bundle/05_spleen_segmentation_lightning.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"outputs": [],
3939
"source": [
4040
"!python -c \"import monai\" || pip install -q \"monai-weekly[ignite,pyyaml]\"\n",
41-
"!pip install -q pytorch-lightning~=2.0.0"
41+
"!pip install -q pytorch-lightning>=2.1"
4242
]
4343
},
4444
{
@@ -855,7 +855,7 @@
855855
"execution_count": 11,
856856
"id": "c5ba337d-a5b0-47de-9ae2-1554a2cb4f86",
857857
"metadata": {},
858-
"outputs": [
858+
"outputs": [
859859
{
860860
"name": "stdout",
861861
"output_type": "stream",

competitions/MICCAI/surgtoolloc/preprocess_detect_scene_and_split_fold.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@
398398
],
399399
"source": [
400400
"for lb in labels:\n",
401-
" print(f\"{lb:30} {df_scene[df_scene[lb]>0].scene.nunique()}\")"
401+
" print(f\"{lb:30} {df_scene[df_scene[lb] > 0].scene.nunique()}\")"
402402
]
403403
},
404404
{

deep_atlas/deep_atlas_tutorial.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,7 @@
12551255
"val_interval = 5\n",
12561256
"\n",
12571257
"for epoch_number in range(max_epochs):\n",
1258-
" print(f\"Epoch {epoch_number+1}/{max_epochs}:\")\n",
1258+
" print(f\"Epoch {epoch_number + 1}/{max_epochs}:\")\n",
12591259
"\n",
12601260
" seg_net.train()\n",
12611261
" losses = []\n",
@@ -1797,7 +1797,7 @@
17971797
"best_reg_validation_loss = float(\"inf\")\n",
17981798
"\n",
17991799
"for epoch_number in range(max_epochs):\n",
1800-
" print(f\"Epoch {epoch_number+1}/{max_epochs}:\")\n",
1800+
" print(f\"Epoch {epoch_number + 1}/{max_epochs}:\")\n",
18011801
"\n",
18021802
" # ------------------------------------------------\n",
18031803
" # reg_net training, with seg_net frozen\n",
@@ -2320,7 +2320,7 @@
23202320
"preview_image(det, normalize_by=\"slice\", threshold=0)\n",
23212321
"loss = lncc_loss(example_warped_image, img12[:, [0], :, :, :]).item()\n",
23222322
"print(f\"Similarity loss: {loss}\")\n",
2323-
"print(f\"number of folds: {(det<=0).sum()}\")\n",
2323+
"print(f\"number of folds: {(det <= 0).sum()}\")\n",
23242324
"\n",
23252325
"del reg_net_example_output, img12, example_warped_image\n",
23262326
"torch.cuda.empty_cache()"

modules/interpretability/class_lung_lesion.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -680,8 +680,8 @@
680680
" name += \"lesion\" if label == 1 else \"non-lesion\"\n",
681681
" name += \"\\npred: \"\n",
682682
" name += \"lesion\" if pred_label == 1 else \"non-lesion\"\n",
683-
" name += f\"\\nlesion: {y_pred[0,1]:.3}\"\n",
684-
" name += f\"\\nnon-lesion: {y_pred[0,0]:.3}\"\n",
683+
" name += f\"\\nlesion: {y_pred[0, 1]:.3}\"\n",
684+
" name += f\"\\nnon-lesion: {y_pred[0, 0]:.3}\"\n",
685685
"\n",
686686
" # run CAM\n",
687687
" cam_result = cam(x=image, class_idx=None)\n",

runner.sh

Lines changed: 146 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" maisi_inference_tut
8484
doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" realism_diversity_metrics.ipynb)
8585
doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" omniverse_integration.ipynb)
8686
doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" hugging_face_pipeline_for_monai.ipynb)
87+
doesnt_contain_max_epochs=("${doesnt_contain_max_epochs[@]}" msd_crossval_datalist_generator.ipynb) # inference/datalist-only notebook, no training loop
8788

8889
# Execution of the notebook in these folders / with the filename cannot be automated
8990
skip_run_papermill=()
@@ -135,6 +136,9 @@ skip_run_papermill=("${skip_run_papermill[@]}" .*learn2reg_oasis_unpaired_brain_
135136
skip_run_papermill=("${skip_run_papermill[@]}" .*finetune_vista3d_for_hugging_face_pipeline.ipynb*)
136137
skip_run_papermill=("${skip_run_papermill[@]}" .*TCIA_PROSTATEx_Prostate_MRI_Anatomy_Model.ipynb*) # https://github.com/Project-MONAI/tutorials/issues/2029
137138
skip_run_papermill=("${skip_run_papermill[@]}" .*maisi_inference_tutorial.ipynb*)
139+
skip_run_papermill=("${skip_run_papermill[@]}" .*image_restoration.ipynb*) # monai.networks.nets.restormer not yet in dev branch
140+
skip_run_papermill=("${skip_run_papermill[@]}" .*05_spleen_segmentation_lightning*) # requires GPU; hardcoded .to("cuda") with no CPU fallback
141+
skip_run_papermill=("${skip_run_papermill[@]}" .*deep_atlas_tutorial*) # requires GPU; device hardcoded to "cuda:0"
138142

139143
# output formatting
140144
separator=""
@@ -160,6 +164,8 @@ autofix=false
160164
failfast=false
161165
pattern=""
162166
papermill_opt=""
167+
jobs=1
168+
data_dir=""
163169

164170
kernelspec="python3"
165171

@@ -169,7 +175,7 @@ NB_OUTPUT_LINE_CAP=100
169175

170176
function print_usage {
171177
echo "runner.sh [--no-run] [--no-checks] [--autofix] [-f/--failfast] [-p/--pattern <find pattern>] [-h/--help]"
172-
echo "[-v/--version] [--verbose]"
178+
echo "[-v/--version] [--verbose] [-j/--jobs <N>] [--data-dir <path>]"
173179
echo ""
174180
echo "MONAI tutorials testing utilities. When running the notebooks, we first search for variables, such as"
175181
echo "\"max_epochs\" and set them to 1 to reduce testing time."
@@ -180,12 +186,21 @@ function print_usage {
180186
echo " --autofix : autofix where possible"
181187
echo " --cell-standard : check guidelines standards such as ## setup environment cell blocks"
182188
echo " --copyright : check whether every source code and notebook has a copyright header"
183-
echo " -f, --failfast : stop on first error"
189+
echo " -f, --failfast : stop on first error (ignored when --jobs > 1)"
184190
echo " -p, --pattern : pattern of files to be run (added to \`find . -type f -name *.ipynb -and ! -wholename *.ipynb_checkpoints*\`)"
185191
echo " -h, --help : show this help message and exit"
186192
echo " -t, --test : shortcut to run a single notebook using pattern \`-and -wholename\`"
187193
echo " -v, --version : show MONAI and system version information and exit"
188-
echo " --verbose : show papermill logs when testing the noteboobks"
194+
echo " --verbose : show papermill logs when testing the notebooks"
195+
echo " -j, --jobs N : run N notebooks in parallel (default: 1). Each notebook logs independently;"
196+
echo " logs are printed in original order after all jobs finish."
197+
echo " Note: parallel jobs share the same Python environment; use --jobs 1 when"
198+
echo " notebooks do conflicting pip installs."
199+
echo " --data-dir PATH : set MONAI_DATA_DIRECTORY to PATH before running notebooks. Notebooks that"
200+
echo " respect this env var will persist downloads there instead of a temp dir."
201+
echo " Tip: mount a host directory at this path in Docker to cache across runs:"
202+
echo " docker run ... -v /host/data:/data -e MONAI_DATA_DIRECTORY=/data ..."
203+
echo " or pass --data-dir /data to this script after the Docker bind-mount."
189204
echo ""
190205
echo "Examples:"
191206
echo "./runner.sh # run full tests (${green}recommended before making pull requests${noColor})."
@@ -197,6 +212,8 @@ function print_usage {
197212
echo " # check filenames containing \"read\" or \"load\", but not if the"
198213
echo " whole path contains \"deepgrow\"."
199214
echo "./runner.sh --kernelspec \"kernel\" # Set the kernelspec value used to run notebooks, default is \"python3\"."
215+
echo "./runner.sh -j 4 --data-dir /data/monai_cache"
216+
echo " # run 4 notebooks in parallel; reuse cached downloads."
200217
echo "./runner.sh --no-checks --no-run --copyright
201218
echo " # test if all notebooks and scripts have the copyright header"
202219
echo "./runner.sh --no-checks --no-run --cell-standard
@@ -247,6 +264,14 @@ do
247264
echo $pattern
248265
shift
249266
;;
267+
-j|--jobs)
268+
jobs="$2"
269+
shift
270+
;;
271+
--data-dir)
272+
data_dir="$2"
273+
shift
274+
;;
250275
-k|--kernelspec)
251276
kernelspec="$2"
252277
shift
@@ -429,6 +454,19 @@ fi
429454
base_path="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
430455
cd "${base_path}"
431456

457+
# Export MONAI_DATA_DIRECTORY so notebooks persist downloads across runs.
458+
# 109/117 runnable notebooks honour this env var (pattern:
459+
# directory = os.environ.get("MONAI_DATA_DIRECTORY")
460+
# root_dir = tempfile.mkdtemp() if directory is None else directory
461+
# Without it each notebook re-downloads on every container run.
462+
if [ -n "$data_dir" ]; then
463+
mkdir -p "$data_dir"
464+
export MONAI_DATA_DIRECTORY="$data_dir"
465+
echo "Data cache: $data_dir (MONAI_DATA_DIRECTORY)"
466+
elif [ -n "${MONAI_DATA_DIRECTORY:-}" ]; then
467+
echo "Data cache: $MONAI_DATA_DIRECTORY (MONAI_DATA_DIRECTORY from environment)"
468+
fi
469+
432470
function replace_text {
433471
oldString="${s}\s*=\s*[0-9]\+"
434472
newString="${s} = 1"
@@ -481,24 +519,28 @@ fi
481519

482520
########################################################################
483521
# #
484-
# loop over files #
522+
# per-notebook logic (used by both sequential and parallel paths) #
485523
# #
486524
########################################################################
487-
for file in "${files[@]}"; do
488-
current_test_successful=0
525+
# _run_notebook FILE RESULT_FILE
526+
# Runs PEP8 checks and/or papermill for FILE.
527+
# Writes 0 (pass) or 1 (fail) to RESULT_FILE.
528+
# Must be called in a subshell so cwd changes are isolated.
529+
function _run_notebook {
530+
local file="$1"
531+
local result_file="$2"
532+
local current_test_successful=0
489533

490534
echo "${separator}${blue}Running $file${noColor}"
491535

492-
# Get to file's folder and get file contents
536+
local path filename
493537
path="$(dirname "${file}")"
494538
filename="$(basename "${file}")"
495-
cd ${base_path}/${path}
539+
cd "${base_path}/${path}"
496540

497-
########################################################################
498-
# #
499-
# code checks #
500-
# #
501-
########################################################################
541+
####################################################################
542+
# code checks #
543+
####################################################################
502544
if [ $doChecks = true ]; then
503545

504546
if [ $autofix = true ]; then
@@ -509,33 +551,26 @@ for file in "${files[@]}"; do
509551
--pipe "sed 's/ = list()/ = []/'"
510552
fi
511553

512-
# to check flake8, convert to python script, don't check
513-
# magic cells, and don't check line length for comment
514-
# lines (as this includes markdown), and then run flake8
515554
echo Checking PEP8 compliance...
516555
jupytext "$filename" --opt custom_cell_magics="writefile" -w --to script -o - | \
517556
sed 's/\(^\s*\)%/\1pass # %/' | \
518557
sed 's/\(^#.*\)$/\1 # noqa: E501/' | \
519558
flake8 - --show-source --extend-ignore=E203,N812,W503 --max-line-length 120
520-
success=$?
521-
if [ ${success} -ne 0 ]
522-
then
559+
local success=$?
560+
if [ ${success} -ne 0 ]; then
523561
print_error_msg "Try running with autofixes: ${green}--autofix${noColor}"
524-
test_fail ${success}
562+
current_test_successful=1
525563
fi
526564
fi
527565

528-
########################################################################
529-
# #
530-
# run notebooks with papermill #
531-
# #
532-
########################################################################
533-
if [ $doRun = true ]; then
534-
535-
skipRun=false
566+
####################################################################
567+
# run notebook with papermill #
568+
####################################################################
569+
if [ $doRun = true ] && [ $current_test_successful -eq 0 ]; then
536570

571+
local skipRun=false
537572
for skip_pattern in "${skip_run_papermill[@]}"; do
538-
if [[ $file =~ $skip_pattern ]]; then
573+
if [[ $file =~ $skip_pattern ]]; then
539574
echo "Skip Pattern Match"
540575
skipRun=true
541576
break
@@ -544,45 +579,112 @@ for file in "${files[@]}"; do
544579

545580
if [ $skipRun = true ]; then
546581
echo "Skipping"
547-
continue
582+
echo "$current_test_successful" > "$result_file"
583+
return
548584
fi
549585

550586
echo Running notebook...
587+
local notebook
551588
notebook=$(cat "$filename")
552589

553-
# if compulsory keyword, max_epochs, missing...
554590
if [[ ! "$notebook" =~ "max_epochs" ]]; then
555-
# and notebook isn't in list of those expected to not have that keyword...
556-
should_contain_max_epochs=true
591+
local should_contain_max_epochs=true
557592
for e in "${doesnt_contain_max_epochs[@]}"; do
558593
[[ "$e" == "$filename" ]] && should_contain_max_epochs=false && break
559594
done
560-
# then error
561595
if [[ $should_contain_max_epochs == true ]]; then
562596
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\")."
563-
test_fail 1
597+
current_test_successful=1
598+
echo "$current_test_successful" > "$result_file"
599+
return
564600
fi
565601
fi
566602

567-
# Set some variables to 1 to speed up proceedings
568-
strings_to_replace=(max_epochs val_interval disc_train_interval disc_train_steps num_batches_for_histogram)
603+
local strings_to_replace=(max_epochs val_interval disc_train_interval disc_train_steps num_batches_for_histogram)
569604
for s in "${strings_to_replace[@]}"; do
570605
replace_text
571606
done
572607

573608
python -c 'import monai; monai.config.print_config()'
574609

610+
local cmd
575611
cmd=$(echo "papermill ${papermill_opt} --progress-bar --log-output -k ${kernelspec}")
576612
echo "$cmd"
613+
local out
577614
time out=$(echo "$notebook" | eval "$cmd")
578-
success=$?
615+
local success=$?
579616
if [[ ${success} -ne 0 || "$out" =~ "\"status\": \"failed\"" ]]; then
580-
test_fail ${success}
617+
current_test_successful=1
581618
fi
582619
fi
583620

584-
num_tested=$((num_tested + 1))
585-
if [[ ${current_test_successful} -eq 0 ]]; then
586-
num_successful_tests=$((num_successful_tests + 1))
587-
fi
588-
done
621+
echo "$current_test_successful" > "$result_file"
622+
}
623+
624+
########################################################################
625+
# #
626+
# loop over files — sequential (jobs=1) or parallel (jobs>1) #
627+
# #
628+
########################################################################
629+
if [ "$jobs" -le 1 ]; then
630+
# ---------------------------------------------------------------- #
631+
# Sequential path — original behaviour, unchanged #
632+
# ---------------------------------------------------------------- #
633+
for file in "${files[@]}"; do
634+
current_test_successful=0
635+
_result_file=$(mktemp)
636+
637+
( trap - EXIT; _run_notebook "$file" "$_result_file" )
638+
639+
current_test_successful=$(cat "$_result_file" 2>/dev/null || echo 1)
640+
rm -f "$_result_file"
641+
642+
num_tested=$((num_tested + 1))
643+
if [[ ${current_test_successful} -eq 0 ]]; then
644+
num_successful_tests=$((num_successful_tests + 1))
645+
elif [ $failfast = true ]; then
646+
finish
647+
fi
648+
done
649+
650+
else
651+
# ---------------------------------------------------------------- #
652+
# Parallel path — N notebooks run concurrently #
653+
# ---------------------------------------------------------------- #
654+
echo "Running ${#files[@]} notebooks with --jobs $jobs"
655+
_work_dir=$(mktemp -d)
656+
657+
for file in "${files[@]}"; do
658+
# Throttle: wait until a slot is free
659+
while [ "$(jobs -rp | wc -l)" -ge "$jobs" ]; do
660+
wait -n 2>/dev/null || sleep 0.2
661+
done
662+
663+
_slug=$(printf '%s' "$file" | tr '/.' '--')
664+
_log="${_work_dir}/${_slug}.log"
665+
_result="${_work_dir}/${_slug}.result"
666+
667+
(
668+
trap - EXIT
669+
set +e
670+
_run_notebook "$file" "$_result"
671+
) > "$_log" 2>&1 &
672+
done
673+
674+
wait # wait for all remaining background jobs
675+
676+
# Print logs in original notebook order; collect pass/fail counts
677+
for file in "${files[@]}"; do
678+
_slug=$(printf '%s' "$file" | tr '/.' '--')
679+
_log="${_work_dir}/${_slug}.log"
680+
_result="${_work_dir}/${_slug}.result"
681+
682+
cat "$_log"
683+
num_tested=$((num_tested + 1))
684+
if [ "$(cat "$_result" 2>/dev/null)" = "0" ]; then
685+
num_successful_tests=$((num_successful_tests + 1))
686+
fi
687+
done
688+
689+
rm -rf "$_work_dir"
690+
fi

0 commit comments

Comments
 (0)