Skip to content

Fix batch inference issues. - #9

Merged
heyufan1995 merged 2 commits into
NVIDIA-Medtech:mainfrom
heyufan1995:main
Mar 26, 2026
Merged

Fix batch inference issues.#9
heyufan1995 merged 2 commits into
NVIDIA-Medtech:mainfrom
heyufan1995:main

Conversation

@heyufan1995

Copy link
Copy Markdown
Contributor

Added supports for inferencing large scale datasets with batch processing.

@heyufan1995
heyufan1995 merged commit 43c3a00 into NVIDIA-Medtech:main Mar 26, 2026
1 check failed
@greptile-apps

greptile-apps Bot commented Mar 26, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds robust large-scale batch inference support across both NV-Segment-CT and NV-Segment-CTMR bundles, replacing the simple single-directory glob with a recursive discovery utility (batch_inference_utils.py) that supports resume (skip already-processed files), folder-name filters, and a rank-0 JSON cache to avoid redundant filesystem scans in multi-GPU (torchrun) runs. The brain MRI shell script is significantly extended with --file_list/--root_path mode, optional skull-stripping (--no-skullstrip), deterministic workload partitioning, a per-file failure log, and per-file timeouts.\n\nKey changes:\n- batch_inference_utils.py (new, CT + CTMR): recursive NIfTI discovery, MONAI SaveImaged-compatible output path prediction for resume, atomic temp-file cache write for multi-GPU coordination, SystemExit(0) escape when nothing remains (avoids empty DistributedSampler crash).\n- configs/batch_inference.json (CT + CTMR): replaces glob expression with build_input_list call; exposes batch_resume_skip_existing, batch_skip_dir_names/prefixes, cache knobs; sets data_root_dir for relative output paths.\n- configs/mgpu_inference.json (CT + CTMR): adds monai.utils.set_determinism(seed=123) and checkpointloader(@evaluator) to the multi-GPU initialize block.\n- run_brain_segmentation.sh: major rewrite — adds file-list mode with partitioning, --no-skullstrip, per-file timeouts, and failure logging; removes conda-env activation (caller's responsibility).\n\nNotable issues found:\n- The hardcoded 5-minute per-file timeout in process_file_list is likely too short for brain MRI workloads that include Docker-based skull stripping.\n- input_suffix in batch_inference.json is retained but silently unused; build_input_list always scans **/*.nii.gz.\n- The ERR trap set inside process_single_file is not reset when the function exits via the failure path (log_failure → return 1), leaving the trap active in the calling shell.

Confidence Score: 4/5

Safe to merge with one targeted fix: the hardcoded per-file timeout should be made configurable or raised before the script ships to production cohort processing.

The core Python batch utility is well-designed (atomic cache, MONAI path prediction, clean multi-GPU coordination). The config changes are minimal and correct. The shell script issues (timeout, ERR trap, stderr merge) are real but won't cause data loss — failures are logged and the resume mechanism lets users retry.

NV-Segment-CTMR/brain_t1_preprocess/run_brain_segmentation.sh — hardcoded timeout, ERR-trap leak, stderr/stdout merge. NV-Segment-CT/scripts/batch_inference_utils.py and its CTMR counterpart — input_suffix not forwarded.

Important Files Changed

Filename Overview
NV-Segment-CT/scripts/batch_inference_utils.py New utility: discovers NIfTI inputs recursively, computes expected MONAI output paths for resume, and coordinates rank-0 cache to avoid redundant FS scans in multi-GPU runs. Core logic is sound but input_suffix from config is silently ignored (hardcoded **/*.nii.gz).
NV-Segment-CTMR/scripts/batch_inference_utils.py Identical to the CT variant—same logic, same input_suffix issue, same atomic cache write pattern.
NV-Segment-CTMR/brain_t1_preprocess/run_brain_segmentation.sh Major rework: adds --file_list/--root_path mode with partitioning, removes conda activation, makes skull-stripping optional, and converts exit to return for per-file failure tolerance. Issues: 5-minute timeout is hardcoded and likely too short for brain MRI; ERR trap leaks into caller on failure path; 2>&1 in timeout subshell merges stderr into stdout.
NV-Segment-CT/configs/batch_inference.json Replaces simple glob with build_input_list call; adds resume/cache/filter knobs; sets data_root_dir for relative output paths. Config looks correct; input_suffix remains but is unused by the new utility.
NV-Segment-CTMR/configs/batch_inference.json Identical change to CT variant—same observations apply.
NV-Segment-CT/configs/mgpu_inference.json Adds set_determinism(seed=123) and checkpointloader(@evaluator) to the multi-GPU initialize list—both are appropriate for deterministic, reproducible distributed inference.
NV-Segment-CTMR/configs/mgpu_inference.json Same as CT variant—determinism seed and checkpoint loader added to multi-GPU initialize.
NV-Segment-CT/scripts/init.py Imports batch_inference_utils so MONAI bundle expressions like scripts.batch_inference_utils.build_input_list resolve correctly.
NV-Segment-CTMR/scripts/init.py Same as CT variant.

Sequence Diagram

sequenceDiagram
    participant U as User / torchrun
    participant R0 as Rank 0 Process
    participant RN as Rank N Process (N>0)
    participant FS as Filesystem / Cache

    U->>R0: torchrun ... monai.bundle run
    U->>RN: torchrun ... monai.bundle run

    Note over R0,RN: initialize: dist.init_process_group, set_device, set_determinism, checkpointloader

    R0->>FS: Recursive glob **/*.nii.gz under input_dir
    R0->>FS: Filter skip_dir_names / skip_dir_prefixes
    R0->>FS: Check expected_output_path exists (resume skip)
    R0->>FS: Write /tmp/nvseg_batch_input_hash.json (atomic rename)

    RN->>FS: Poll cache file (up to batch_cache_wait_sec=120s)
    FS-->>RN: Return cached path list

    alt Nothing to run (all outputs exist)
        R0->>R0: SystemExit(0)
        RN->>RN: SystemExit(0)
    else Files to process
        R0->>R0: Build input_dicts → DistributedSampler
        RN->>RN: Build input_dicts → DistributedSampler
        R0->>FS: SaveImaged → output_dir/.../stem/stem_trans.nii.gz
        RN->>FS: SaveImaged → output_dir/.../stem/stem_trans.nii.gz
    end
Loading

Comments Outside Diff (2)

  1. NV-Segment-CTMR/brain_t1_preprocess/run_brain_segmentation.sh, line 896 (link)

    P1 Hardcoded 5-minute timeout may be too short

    timeout_seconds=300 (5 minutes) is hardcoded per file and not user-configurable via CLI. Brain MRI processing involves Docker-based skull stripping (synthstrip-docker), affine alignment, and MONAI bundle inference. On first run, Docker may need to pull the SynthStrip image, and large volumes or slow storage can easily push a single case past 5 minutes. When the timeout fires, the file is written to the failed_log and the caller must re-run manually.

    Consider exposing this as a --timeout CLI argument so users can tune it for their hardware, or at least raise the default significantly.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

  2. NV-Segment-CTMR/brain_t1_preprocess/run_brain_segmentation.sh, line 914-917 (link)

    P2 2>&1 in the timeout subshell merges stderr into stdout

    timeout ... bash -c "..." 2>&1 redirects all stderr from process_single_file (which explicitly uses >&2 for every log line) back to stdout of the parent. This means diagnostic messages such as "Processing:", "Step 1/4:", etc. appear on the parent's stdout rather than stderr, which can pollute any pipeline that consumes stdout (e.g., logging systems that distinguish the two streams).

    Consider removing the 2>&1 to preserve the stderr/stdout distinction:

Reviews (1): Last reviewed commit: "Merge branch 'main' of github.com:heyufa..." | Re-trigger Greptile

Comment on lines +69 to +86
def collect_input_paths(
input_root: Path,
pattern: str = "**/*.nii.gz",
*,
skip_dir_names: list[str] | None = None,
skip_dir_prefixes: list[str] | None = None,
) -> list[Path]:
input_root = input_root.resolve()
out: list[Path] = []
for p in sorted(input_root.glob(pattern)):
if not p.is_file():
continue
if should_skip_path_by_parent_rules(
p, skip_dir_names=skip_dir_names, skip_dir_prefixes=skip_dir_prefixes
):
continue
out.append(p)
return out

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 input_suffix config value is silently ignored

collect_input_paths hardcodes the glob pattern "**/*.nii.gz", which means the input_suffix key in configs/batch_inference.json (set to "*.nii.gz") has no effect on batch discovery. Two consequences:

  1. Users who change input_suffix to, e.g., "*.nii" will not see plain (non-gzipped) NIfTI files discovered—build_input_list never passes the suffix through.
  2. The same silent mis-match applies to the identical file in NV-Segment-CTMR/scripts/batch_inference_utils.py.

Either accept and forward input_suffix (or a pattern parameter) from build_input_list down to collect_input_paths, or document prominently in batch_inference.json that input_suffix is unused for the batch utility and only **/*.nii.gz is ever matched.

Comment on lines +521 to +527
((fail_count++))
process_result=1
fi
else
# Fallback: run without timeout wrapper
# Note: Without timeout command, we can't enforce the 5-minute limit
# but we can still catch failures

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 ERR trap not reset on the failure path

trap cleanup_on_error ERR is set inside process_single_file. The matching trap - ERR is placed at the very end of the success path (after all steps complete). However, when log_failure is called and return 1 exits the function early, trap - ERR is never reached. In Bash, trap is global to the shell (not scoped to functions), so the cleanup_on_error trap remains active in the calling shell after process_single_file returns with an error code.

In process_folder, subsequent arithmetic expansions like ((fail_count++)) evaluate to exit code 1 when the counter is 0, which would unexpectedly fire cleanup_on_error against the temp directory of the just-failed case (likely already removed, so harmless, but still unintended).

Reset the trap before every early return:

log_failure() {
    local reason="$1"
    echo -e "${RED}$reason: $input_file${NC}" >&2
    if [[ -n "$failed_log" ]]; then
        echo "$input_file" >> "$failed_log"
    fi
    cleanup_on_error
    trap - ERR   # reset here so the caller's shell is clean
    return 1
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant