Skip to content

Commit d599eb7

Browse files
garciadiasclaude
andcommitted
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 <noreply@anthropic.com>
1 parent d38d101 commit d599eb7

1 file changed

Lines changed: 142 additions & 44 deletions

File tree

runner.sh

Lines changed: 142 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ autofix=false
164164
failfast=false
165165
pattern=""
166166
papermill_opt=""
167+
jobs=1
168+
data_dir=""
167169

168170
kernelspec="python3"
169171

@@ -173,7 +175,7 @@ NB_OUTPUT_LINE_CAP=100
173175

174176
function print_usage {
175177
echo "runner.sh [--no-run] [--no-checks] [--autofix] [-f/--failfast] [-p/--pattern <find pattern>] [-h/--help]"
176-
echo "[-v/--version] [--verbose]"
178+
echo "[-v/--version] [--verbose] [-j/--jobs <N>] [--data-dir <path>]"
177179
echo ""
178180
echo "MONAI tutorials testing utilities. When running the notebooks, we first search for variables, such as"
179181
echo "\"max_epochs\" and set them to 1 to reduce testing time."
@@ -184,12 +186,21 @@ function print_usage {
184186
echo " --autofix : autofix where possible"
185187
echo " --cell-standard : check guidelines standards such as ## setup environment cell blocks"
186188
echo " --copyright : check whether every source code and notebook has a copyright header"
187-
echo " -f, --failfast : stop on first error"
189+
echo " -f, --failfast : stop on first error (ignored when --jobs > 1)"
188190
echo " -p, --pattern : pattern of files to be run (added to \`find . -type f -name *.ipynb -and ! -wholename *.ipynb_checkpoints*\`)"
189191
echo " -h, --help : show this help message and exit"
190192
echo " -t, --test : shortcut to run a single notebook using pattern \`-and -wholename\`"
191193
echo " -v, --version : show MONAI and system version information and exit"
192-
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."
193204
echo ""
194205
echo "Examples:"
195206
echo "./runner.sh # run full tests (${green}recommended before making pull requests${noColor})."
@@ -201,6 +212,8 @@ function print_usage {
201212
echo " # check filenames containing \"read\" or \"load\", but not if the"
202213
echo " whole path contains \"deepgrow\"."
203214
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."
204217
echo "./runner.sh --no-checks --no-run --copyright
205218
echo " # test if all notebooks and scripts have the copyright header"
206219
echo "./runner.sh --no-checks --no-run --cell-standard
@@ -251,6 +264,14 @@ do
251264
echo $pattern
252265
shift
253266
;;
267+
-j|--jobs)
268+
jobs="$2"
269+
shift
270+
;;
271+
--data-dir)
272+
data_dir="$2"
273+
shift
274+
;;
254275
-k|--kernelspec)
255276
kernelspec="$2"
256277
shift
@@ -433,6 +454,19 @@ fi
433454
base_path="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
434455
cd "${base_path}"
435456

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+
436470
function replace_text {
437471
oldString="${s}\s*=\s*[0-9]\+"
438472
newString="${s} = 1"
@@ -485,24 +519,28 @@ fi
485519

486520
########################################################################
487521
# #
488-
# loop over files #
522+
# per-notebook logic (used by both sequential and parallel paths) #
489523
# #
490524
########################################################################
491-
for file in "${files[@]}"; do
492-
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
493533

494534
echo "${separator}${blue}Running $file${noColor}"
495535

496-
# Get to file's folder and get file contents
536+
local path filename
497537
path="$(dirname "${file}")"
498538
filename="$(basename "${file}")"
499-
cd ${base_path}/${path}
539+
cd "${base_path}/${path}"
500540

501-
########################################################################
502-
# #
503-
# code checks #
504-
# #
505-
########################################################################
541+
####################################################################
542+
# code checks #
543+
####################################################################
506544
if [ $doChecks = true ]; then
507545

508546
if [ $autofix = true ]; then
@@ -513,33 +551,26 @@ for file in "${files[@]}"; do
513551
--pipe "sed 's/ = list()/ = []/'"
514552
fi
515553

516-
# to check flake8, convert to python script, don't check
517-
# magic cells, and don't check line length for comment
518-
# lines (as this includes markdown), and then run flake8
519554
echo Checking PEP8 compliance...
520555
jupytext "$filename" --opt custom_cell_magics="writefile" -w --to script -o - | \
521556
sed 's/\(^\s*\)%/\1pass # %/' | \
522557
sed 's/\(^#.*\)$/\1 # noqa: E501/' | \
523558
flake8 - --show-source --extend-ignore=E203,N812,W503 --max-line-length 120
524-
success=$?
525-
if [ ${success} -ne 0 ]
526-
then
559+
local success=$?
560+
if [ ${success} -ne 0 ]; then
527561
print_error_msg "Try running with autofixes: ${green}--autofix${noColor}"
528-
test_fail ${success}
562+
current_test_successful=1
529563
fi
530564
fi
531565

532-
########################################################################
533-
# #
534-
# run notebooks with papermill #
535-
# #
536-
########################################################################
537-
if [ $doRun = true ]; then
538-
539-
skipRun=false
566+
####################################################################
567+
# run notebook with papermill #
568+
####################################################################
569+
if [ $doRun = true ] && [ $current_test_successful -eq 0 ]; then
540570

571+
local skipRun=false
541572
for skip_pattern in "${skip_run_papermill[@]}"; do
542-
if [[ $file =~ $skip_pattern ]]; then
573+
if [[ $file =~ $skip_pattern ]]; then
543574
echo "Skip Pattern Match"
544575
skipRun=true
545576
break
@@ -548,45 +579,112 @@ for file in "${files[@]}"; do
548579

549580
if [ $skipRun = true ]; then
550581
echo "Skipping"
551-
continue
582+
echo "$current_test_successful" > "$result_file"
583+
return
552584
fi
553585

554586
echo Running notebook...
587+
local notebook
555588
notebook=$(cat "$filename")
556589

557-
# if compulsory keyword, max_epochs, missing...
558590
if [[ ! "$notebook" =~ "max_epochs" ]]; then
559-
# and notebook isn't in list of those expected to not have that keyword...
560-
should_contain_max_epochs=true
591+
local should_contain_max_epochs=true
561592
for e in "${doesnt_contain_max_epochs[@]}"; do
562593
[[ "$e" == "$filename" ]] && should_contain_max_epochs=false && break
563594
done
564-
# then error
565595
if [[ $should_contain_max_epochs == true ]]; then
566596
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\")."
567-
test_fail 1
597+
current_test_successful=1
598+
echo "$current_test_successful" > "$result_file"
599+
return
568600
fi
569601
fi
570602

571-
# Set some variables to 1 to speed up proceedings
572-
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)
573604
for s in "${strings_to_replace[@]}"; do
574605
replace_text
575606
done
576607

577608
python -c 'import monai; monai.config.print_config()'
578609

610+
local cmd
579611
cmd=$(echo "papermill ${papermill_opt} --progress-bar --log-output -k ${kernelspec}")
580612
echo "$cmd"
613+
local out
581614
time out=$(echo "$notebook" | eval "$cmd")
582-
success=$?
615+
local success=$?
583616
if [[ ${success} -ne 0 || "$out" =~ "\"status\": \"failed\"" ]]; then
584-
test_fail ${success}
617+
current_test_successful=1
585618
fi
586619
fi
587620

588-
num_tested=$((num_tested + 1))
589-
if [[ ${current_test_successful} -eq 0 ]]; then
590-
num_successful_tests=$((num_successful_tests + 1))
591-
fi
592-
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)