Multi-Step SLURM Workflows
Last updated on 2026-08-27 | Edit this page
Estimated time: 50 minutes
Overview
Questions
- How do I integrate Make with SLURM for automated job submission?
- How do I ensure HPC workflows are reproducible?
- How do I monitor and debug multi-job workflows?
- How do I make pipeline reruns idempotent?
Objectives
- Integrate Make with SLURM for automated, reproducible job submission
- Capture environment state and input checksums for reproducibility
- Monitor and debug multi-job workflows on Sagehen HPC
- Make pipelines idempotent so reruns do not waste compute
Why Multi-Step Pipelines Need a Pipeline Tool
Real research workflows almost never fit in one SLURM script. Preprocessing, training, and analysis each have different resource needs (CPU vs GPU, memory, time), and they must run in order. Doing this by hand means submitting one job, checking its status, manually submitting the next, and so on. The first time you forget to submit step 3 because you went to bed at 2 AM, you understand why pipeline tools exist.
Make is the simplest tool for this on HPC. It tracks dependencies
between targets, only re-runs what has changed, and can submit SLURM
jobs as part of building a target. The result: one make
command that submits the whole pipeline, with proper dependencies
between steps, and re-running it skips already-finished work.
Integration: Make Submitting SLURM Jobs
The most reproducible approach: use Make to orchestrate SLURM job submission.
MAKEFILE
# HPC-aware Makefile for gene expression pipeline
PYTHON := python3
# --parsable makes sbatch print ONLY the job ID (not "Submitted batch job 12345"),
# so the .jid files below contain something --dependency=afterok: can actually use.
SLURM := sbatch --parsable
SLURM_ARGS := --time=04:00:00 --cpus-per-task=8 --mem=32G
# Track job completion
JOB_DIR := .jobs
PREPROCESS_JOB := $(JOB_DIR)/preprocess.jid
ANALYSIS_JOBS := $(JOB_DIR)/analysis.jid
MERGE_JOB := $(JOB_DIR)/merge.jid
FIGURES_JOB := $(JOB_DIR)/figures.jid
.PHONY: all clean clean-all help status
all: $(FIGURES_JOB)
@echo "Workflow submitted. Use 'make status' to check progress."
$(JOB_DIR):
mkdir -p $(JOB_DIR)
# Step 1: Preprocess (must run first)
$(PREPROCESS_JOB): $(JOB_DIR) | data/raw/data.csv
@echo "Submitting preprocessing job..."
$(SLURM) $(SLURM_ARGS) --output=logs/preprocess.log \
--job-name=preprocess \
scripts/preprocess.sh > $@
@echo "Preprocessing JID: $$(cat $@)"
# Step 2: Analysis (depends on preprocessing, runs in parallel)
$(ANALYSIS_JOBS): $(PREPROCESS_JOB)
@echo "Submitting analysis jobs..."
PJID=$$(cat $(PREPROCESS_JOB)) && \
$(SLURM) $(SLURM_ARGS) --output=logs/analysis_%a.log \
--job-name=analysis \
--array=1-100 \
--dependency=afterok:$$PJID \
scripts/analyze.sh > $@
# Step 3: Merge results (depends on all analyses)
$(MERGE_JOB): $(ANALYSIS_JOBS)
@echo "Submitting merge job..."
AJID=$$(cat $(ANALYSIS_JOBS)) && \
$(SLURM) $(SLURM_ARGS) --output=logs/merge.log \
--job-name=merge \
--dependency=afterok:$$AJID \
scripts/merge.sh > $@
# Step 4: Generate figures (depends on merge)
$(FIGURES_JOB): $(MERGE_JOB)
@echo "Submitting figures job..."
MJID=$$(cat $(MERGE_JOB)) && \
$(SLURM) $(SLURM_ARGS) --output=logs/figures.log \
--job-name=figures \
--dependency=afterok:$$MJID \
scripts/figures.sh > $@
Two ideas in this Makefile do the heavy lifting. First, each step’s
“target” is a small file (.jobs/preprocess.jid) containing
the SLURM job ID, not the actual results. Make tracks these JID files
and uses them to express dependencies. Second, each step’s
sbatch call uses --dependency=afterok:$$PJID
to wait for the previous step to finish before starting. SLURM enforces
the ordering; Make just submits them in the right sequence.
The result: make submits the whole pipeline in seconds
and returns. SLURM runs the jobs in dependency order over the next
several hours. You can make status any time to see where
things stand.
Monitoring Multi-Job Workflows
Add a status target to your Makefile:
MAKEFILE
status:
@echo "=== Pipeline Status ===" && \
squeue -u $$USER -h --format="%.10i %.10j %.2t" | \
grep -E "preprocess|analyze|merge|figures" || \
echo "(No jobs found)"
clean:
rm -rf results/ .jobs
clean-all: clean
rm -rf logs/
help:
@echo "Reproducible HPC Pipeline" && \
echo " make Submit full pipeline" && \
echo " make status Check job status" && \
echo " make clean Remove results"
make status shows the SLURM queue filtered to just this
pipeline’s jobs. Useful for at-a-glance “where am I in the pipeline?”
without parsing squeue output by hand.
The clean target removes results and the
.jobs directory; running make again resubmits
the whole pipeline from scratch. clean-all also removes
logs, useful when starting truly fresh.
Idempotent reruns
A pipeline rerun should be safe: rerunning make after a
partial completion should pick up where things stopped, not redo
everything.
The Makefile pattern above is mostly idempotent because Make checks
file modification times. If .jobs/preprocess.jid exists and
is newer than the script, Make skips that step.
For full idempotence, the underlying scripts must also be idempotent.
A preprocessing script that always overwrites processed.csv
is idempotent. One that appends to a log file is not (rerunning
duplicates the entries). Design scripts to write fresh outputs and never
accumulate state across runs.
Reproducibility in HPC Workflows
To ensure HPC workflows are reproducible:
Document Environment
BASH
# In job scripts
module list > logs/modules_$(date +%Y%m%d).txt
conda env export > logs/environment_$(date +%Y%m%d).yml
Six months later when a reviewer asks “which version of Python
produced these results?”, the captured environment.yml is
the answer.
Record Exact Inputs
BASH
# Archive input data state
find data/processed -name "*.csv" -exec md5sum {} \; > logs/input_checksums.txt
If someone replaces data.csv with a different version a
year later, the saved checksums let you detect that the inputs are no
longer the ones the original results were derived from.
Include Computation Metadata
BASH
# In your results files, record provenance
echo "Generated: $(date)" >> results/metadata.txt
echo "Hostname: $(hostname)" >> results/metadata.txt
echo "SLURM Job ID: $SLURM_JOB_ID" >> results/metadata.txt
echo "Git commit: $(git rev-parse HEAD)" >> results/metadata.txt
The git commit hash is essentially mandatory. Without it, “the code that produced result.csv” is undefined six months later when the repo has moved on.
Verify Reproducibility
BASH
# Re-run same job with same inputs
sbatch job_script.sh
# Compare outputs to original run
diff original_results.csv new_results.csv
If the diff is non-empty, your pipeline has a non-determinism somewhere (random seeds not pinned, parallel order affecting numerics, environment drift). Tracking these down is tedious but worthwhile for any result you plan to publish.
Example: Complete Reproducible HPC Pipeline
A real research project structure:
project/
├── README.md
├── Makefile
├── environment.yml
├── scripts/
│ ├── preprocess.sh
│ ├── analyze.sh
│ ├── merge.sh
│ └── figures.sh
├── src/
│ ├── preprocess.py
│ ├── analyze.py
│ ├── merge.py
│ └── visualize.py
├── data/
│ ├── raw/README.md
│ └── processed/
├── results/
└── logs/
The Makefile:
MAKEFILE
.PHONY: all status clean help
JOB_DIR := .jobs
all: $(JOB_DIR)/figures.jid
@echo "Full pipeline submitted to SLURM"
@echo "Track progress: make status"
$(JOB_DIR):
@mkdir -p $(JOB_DIR) logs
$(JOB_DIR)/preprocess.jid: $(JOB_DIR) data/raw/*.csv
@sbatch --job-name=preprocess \
--output=logs/preprocess.log \
--time=02:00:00 --mem=16G \
scripts/preprocess.sh > $@
$(JOB_DIR)/analyze.jid: $(JOB_DIR)/preprocess.jid
@PJID=$$(cat $<) && \
sbatch --job-name=analyze \
--array=1-100 \
--output=logs/analyze_%a.log \
--time=01:00:00 --mem=8G \
--dependency=afterok:$$PJID \
scripts/analyze.sh > $@
$(JOB_DIR)/merge.jid: $(JOB_DIR)/analyze.jid
@AJID=$$(cat $<) && \
sbatch --job-name=merge \
--output=logs/merge.log \
--dependency=afterok:$$AJID \
scripts/merge.sh > $@
$(JOB_DIR)/figures.jid: $(JOB_DIR)/merge.jid
@MJID=$$(cat $<) && \
sbatch --job-name=figures \
--output=logs/figures.log \
--dependency=afterok:$$MJID \
scripts/figures.sh > $@
status:
@squeue -u $$USER -h --format="%.10i %.10j %.2t" | \
grep -E "preprocess|analyze|merge|figures" || \
echo "(No jobs found)"
clean:
@rm -rf results/ .jobs
help:
@echo " make Submit full pipeline"
@echo " make status Check job status"
@echo " make clean Remove results"
Beyond Make: Snakemake and Nextflow
For more sophisticated pipelines (hundreds of steps, conditional branches, integration with cluster scheduling beyond SLURM), specialized workflow managers like Snakemake (Python-based) and Nextflow (Groovy-based) are worth considering. Both have native SLURM integration and richer dependency tracking than Make.
The tradeoff is complexity: Make is six lines of Makefile per step, with no extra runtime. Snakemake adds a Python-based DSL and a runtime daemon. Use Make until you outgrow it; only adopt a workflow manager when the project is large enough to justify the learning curve.
Exercise: Design a Multi-Step HPC Workflow
Sketch a Makefile for an HPC pipeline with these steps:
- Preprocess – 1 job, 2 hours, 16 GB RAM
- Analyze – 50 parallel jobs (array), 1 hour each, 8 GB RAM
- Summarize – 1 job, depends on all analysis jobs
For each step, specify:
- SLURM resource requests
- Dependency relationships
- Input and output verification in the job scripts
MAKEFILE
JOB_DIR := .jobs
$(JOB_DIR)/preprocess.jid:
@mkdir -p $(JOB_DIR) logs
sbatch --time=02:00:00 --mem=16G \
--job-name=preprocess \
--output=logs/preprocess.log \
scripts/preprocess.sh > $@
$(JOB_DIR)/analyze.jid: $(JOB_DIR)/preprocess.jid
@PJID=$$(cat $<) && \
sbatch --time=01:00:00 --mem=8G \
--job-name=analyze --array=1-50 \
--output=logs/analyze_%a.log \
--dependency=afterok:$$PJID \
scripts/analyze.sh > $@
$(JOB_DIR)/summarize.jid: $(JOB_DIR)/analyze.jid
@AJID=$$(cat $<) && \
sbatch --time=00:30:00 --mem=4G \
--job-name=summarize \
--output=logs/summarize.log \
--dependency=afterok:$$AJID \
scripts/summarize.sh > $@
Each job script should include set -euo pipefail, input
file checks ([ ! -f "$INPUT" ] && exit 1), and
output verification. The dependency chain ensures preprocess finishes
before analysis starts, and all 50 analysis jobs finish before summarize
runs.
Exercise: Audit a Pipeline for Reproducibility
You inherit a colleague’s Makefile that produces
results/figure_3.pdf. List four things you would check or
add to make the pipeline fully reproducible six months later when they
have left the lab.
Verify that an
environment.yml(or similar) is committed and matches what the pipeline actually uses. Recreate the environment and check that runningmakeproduces identical output.Check that input data is either committed (small) or has documented provenance with checksums (large). Replace any “find this CSV on Box somewhere” notes with explicit paths and md5 hashes.
Add git commit hashes to result file metadata so future-you can map any result back to the code that produced it.
Run the pipeline twice and
diffthe outputs. Any non-zero diff means there is non-determinism that would prevent reproduction; pin the offending random seed or sort order.
- Integrate Make with SLURM for automated, reproducible job submission
- Track job IDs in files so downstream jobs can declare dependencies
- Use
--dependency=afterok:JIDso SLURM enforces ordering between steps - Capture environment state, input checksums, and compute metadata for reproducibility
- Make HPC workflows trackable with clear logging and a
make statustarget - Idempotent reruns require both Make’s modification-time tracking and idempotent scripts
- A complete HPC pipeline includes scripts, src, data, results, and logs directories
- Verify reproducibility by re-running and diffing outputs; non-zero diff means hidden non-determinism