HPC / SLURM Array Jobs¶
NeuroDAGs processes files independently — each file is self-contained. This maps naturally onto SLURM array jobs: one array task per file, all tasks running in parallel across nodes.
The key parameter is only_index, which restricts a run to specific file indices from the full file list. Combined with $SLURM_ARRAY_TASK_ID, each array task processes exactly one file.
Generating submission scripts¶
Use neurodags slurm-script to generate a ready-to-edit SLURM template populated with your pipeline’s actual derivative names and dependency order:
# per-file (default) — all derivatives per file task
neurodags slurm-script pipeline.yml --output run_array.sh
# flat — file x derivative flat array (max parallelism, independent derivatives only)
neurodags slurm-script pipeline.yml --pattern flat --output run_array_per_deriv.sh
# chained — per-derivative sequential arrays linked via --dependency=afterok
neurodags slurm-script pipeline.yml --pattern chained --output submit_pipeline.sh
The chained pattern writes two files: submit_pipeline.sh (the submission script) and run_one_derivative.sh (the worker). Edit the #SBATCH resource lines before submitting.
Counting source files¶
neurodags count-inputs prints the number of input entries the pipeline will process, i.e. the size of the index range --only-index accepts (0 to N-1). A file listed under two dataset entries counts twice, because it has two indices. Useful in submission scripts:
N=$(neurodags count-inputs pipeline.yml)
sbatch --array=0-$((N - 1)) run_array.sh
Estimating output file count¶
Clusters often enforce per-job or total output file quotas. Before submitting, estimate how many output files the pipeline will produce:
neurodags status pipeline.yml --format json
The JSON output contains:
n_files— number of source (input) filesgrand_total.total— total(derivative × source-file)pairs across all derivatives withsave: Truegrand_total.total / n_files— estimated output files per source file
# estimated output files per source file and total
neurodags status pipeline.yml --format json \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
per_file = d['grand_total']['total'] // d['n_files']
print(f\"derivatives per file : {per_file}\")
print(f\"estimated total files: {d['grand_total']['total']}\")
"
Caveat: these counts are derivative instances, not actual output files. Most derivatives write exactly one file, so the estimate is reliable for typical pipelines. Derivatives that return multiple artifacts (multiple keys in NodeResult.artifacts) will produce more files than this estimate — inspect those nodes manually if precise quota compliance is required.
per-file: One Array Task per File (All Derivatives)¶
Each task runs all derivatives in dependency order for a single file.
N=$(neurodags count-inputs pipeline.yml)
sbatch --array=0-$((N - 1)) run_array.sh
#!/bin/bash
# run_array.sh
#SBATCH --job-name=neurodags
#SBATCH --time=02:00:00
#SBATCH --mem=16G
#SBATCH --cpus-per-task=1
#SBATCH --output=logs/neurodags_%A_%a.out
#SBATCH --error=logs/neurodags_%A_%a.err
source activate myenv
python - <<'EOF'
import os
from neurodags.loaders import load_configuration
from neurodags.orchestrators import run_pipeline
config = load_configuration("pipeline.yml")
run_pipeline(
config,
only_index=int(os.environ["SLURM_ARRAY_TASK_ID"]),
raise_on_error=True,
skip_errors=True,
)
EOF
raise_on_error=True makes the SLURM task exit with a non-zero code on failure, so sacct correctly reports failed tasks. skip_errors=True skips files whose previous run wrote a .error marker.
flat: One Array Task per File × Derivative¶
Use this when derivatives are independent (no inter-derivative dependencies) and you want maximum parallelism.
N=$(neurodags count-inputs pipeline.yml)
N_DERIVATIVES=3
TOTAL=$(( N * N_DERIVATIVES ))
sbatch --array=0-$((TOTAL - 1)) run_array_per_deriv.sh
#!/bin/bash
# run_array_per_deriv.sh
#SBATCH --job-name=neurodags
#SBATCH --time=02:00:00
#SBATCH --mem=8G
#SBATCH --output=logs/neurodags_%A_%a.out
# Task ID encodes both file index and derivative index
# e.g. 3 derivatives × N files → array size = 3 × N
# SLURM_ARRAY_TASK_ID = file_index * n_derivatives + derivative_index
DERIVATIVES=("Preprocessed" "Spectrum" "BandPower")
N_DERIVATIVES=${#DERIVATIVES[@]}
FILE_INDEX=$(( SLURM_ARRAY_TASK_ID / N_DERIVATIVES ))
DERIV_INDEX=$(( SLURM_ARRAY_TASK_ID % N_DERIVATIVES ))
DERIVATIVE=${DERIVATIVES[$DERIV_INDEX]}
source activate myenv
python - <<EOF
from neurodags.loaders import load_configuration
from neurodags.orchestrators import run_pipeline
config = load_configuration("pipeline.yml")
run_pipeline(
config,
derivatives=["$DERIVATIVE"],
only_index=$FILE_INDEX,
raise_on_error=True,
)
EOF
Note: When derivatives have inter-dependencies (e.g.
SpectrumreadsPreprocessedoutput), this pattern requiresPreprocessedto finish beforeSpectrumstarts. Use SLURM--dependency=afterokbetween derivative-level job arrays, or use Pattern 1 which respects dependency order automatically.
chained: Per-Derivative Sequential Array¶
Run derivatives one at a time in dependency order, with all files parallelised within each derivative. Use SLURM job dependencies to chain them.
#!/bin/bash
# submit_pipeline.sh
N=$(neurodags count-inputs pipeline.yml)
ARRAY="0-$((N - 1))"
JOB1=$(sbatch --parsable --array=$ARRAY \
--export=DERIVATIVE=Preprocessed \
run_one_derivative.sh)
JOB2=$(sbatch --parsable --array=$ARRAY \
--dependency=afterok:$JOB1 \
--export=DERIVATIVE=Spectrum \
run_one_derivative.sh)
sbatch --array=$ARRAY \
--dependency=afterok:$JOB2 \
--export=DERIVATIVE=BandPower \
run_one_derivative.sh
#!/bin/bash
# run_one_derivative.sh
#SBATCH --job-name=neurodags
#SBATCH --time=01:00:00
#SBATCH --mem=8G
#SBATCH --output=logs/%x_%A_%a.out
source activate myenv
python - <<EOF
import os
from neurodags.loaders import load_configuration
from neurodags.orchestrators import run_pipeline
config = load_configuration("pipeline.yml")
run_pipeline(
config,
derivatives=["$DERIVATIVE"],
only_index=int(os.environ["SLURM_ARRAY_TASK_ID"]),
raise_on_error=True,
skip_errors=True,
)
EOF
Resubmitting missing/errored files¶
neurodags status reports the global file index of every missing or errored file — the
same index the per-file and chained arrays use as SLURM_ARRAY_TASK_ID (see
Inspection and Visualization). --format indices prints just those indices, space-separated:
# --exit-zero: exit 0 even when something is missing/errored, but still fail on a
# broken config, so `set -e` keeps protecting you
IDX=$(neurodags status pipeline.yml --format indices --exit-zero)
[ -z "$IDX" ] && { echo "nothing to resubmit"; exit 0; }
# run them locally / in one job ...
neurodags run pipeline.yml --only-index $IDX
# ... or as a sparse per-file array (sbatch wants a comma-separated list)
sbatch --array=$(echo "$IDX" | tr ' ' ',') retry_array.sh
Report / resubmission jobs chained with --dependency=afterok. By default status exits
1 whenever anything is missing or errored, which is the expected state after a partial run
or a staged array — so a report job would fail and SLURM would cancel every job chained after
it. Use --exit-zero in such jobs: status then exits 0 whenever the report was produced,
in every --format, while real failures (missing or invalid config, unset environment
variable, unreadable -d file) still exit non-zero and correctly break the chain.
REPORT=$(sbatch --parsable --dependency=afterok:$COMPUTE_JOB \
--wrap "neurodags status pipeline.yml --format json --exit-zero > status.json")
Do not use || true for this (as older versions of this guide did): it also turns a broken
config or an unset $VAR into a “successful” job with an empty or misleading report.
Narrow the list with --list-missing or --list-errors, and per derivative with
--derivative (for the chained pattern, resubmit each derivative’s array with its own list:
neurodags status pipeline.yml --derivative Spectrum --format indices).
--format json carries the same lists as missing_indices / errored_indices, and
--format json --include-files adds one record per file × derivative with the index,
dataset and path, so a script can verify the index → path mapping before submitting.
Things to keep in mind:
Retry workers must not use
skip_errors=True. The per-file and chained templates above (and the onesneurodags slurm-scriptemits) passskip_errors=True, which skips exactly the errored files you are trying to retry. Use a copy of the worker without it (the defaultskip_errors=Falseretries files with a.errormarker), e.g.retry_array.sh=run_array.shminus theskip_errors=Trueline. If a failed run left partial outputs next to its.errormarker, delete them (or setoverwrite) so the retry actually recomputes.Flat-array task IDs are not file indices. In the flat pattern
SLURM_ARRAY_TASK_ID = file_index * N_DERIVATIVES + derivative_index, so never pass status indices straight torun_array_per_deriv.sh. Convert them per derivative, using the position of the derivative in that worker’sDERIVATIVESarray:DERIVATIVES=("Preprocessed" "Spectrum" "BandPower") N_DERIVATIVES=${#DERIVATIVES[@]} TASKS=() for D in "${!DERIVATIVES[@]}"; do for F in $(neurodags status pipeline.yml --derivative "${DERIVATIVES[$D]}" \ --format indices --exit-zero); do TASKS+=($(( F * N_DERIVATIVES + D ))) done done [ ${#TASKS[@]} -gt 0 ] && sbatch --array=$(IFS=,; echo "${TASKS[*]}") run_array_per_deriv.sh
Indices are positions, not IDs. They are stable only while the dataset configuration and the set of input files stay the same — compute the list right before resubmitting, on the same filesystem/mount point the jobs will use.
Checking Failed Tasks¶
After a run, find failed files via .error markers:
find derivatives/ -name "*.error" | sort
Each .error file contains the derivative name, step, and exception. To retry only failed files, delete their .error markers and resubmit — or use skip_errors=False (the default) which retries them automatically.
To skip known-bad files and process only new/uncached ones:
run_pipeline(config, only_index=index, skip_errors=True)
Tips¶
Concern |
Recommendation |
|---|---|
Cluster paths differ from local |
Set |
Node has multiple cores |
Set |
Partial reruns |
Caching is automatic — resubmit the full array, cached files are skipped |
Debugging |
Run |
Array size limit |
Most clusters cap at 1000–10000 tasks; split |