| 32 | | # Var1=... VarN=... sbatch checkpoint_runner.sh |
| 33 | | ############################################################################### |
| | 46 | # Var1=... VarN=... sbatch checkpoint_submitter.sh |
| | 47 | ############################################################################### |
| | 48 | CKPT_PATH="${CKPT_PATH:-state_iter.txt}" |
| | 49 | MAX_ITER="${MAX_ITER:-500}" |
| | 50 | RUNNER_SCRIPT="${RUNNER_SCRIPT:-checkpoint_runner.sh}" |
| | 51 | SUBMITTER_MARGIN_SEC="${SUBMITTER_MARGIN_SEC:-60}" |
| | 52 | SUBMITTER_TIME_LIMIT="${SUBMITTER_TIME_LIMIT:-3:30}" |
| | 53 | ############################################################################### |
| | 54 | # Slurm directives (keep SUBMITTER_TIME_LIMIT <= 24 hours) |
| | 55 | ############################################################################### |
| | 56 | #SBATCH --job-name=ckpt_submitter |
| | 57 | #SBATCH --output=log_submitter_%j.out |
| | 58 | #SBATCH --error=log_submitter_%j.err |
| | 59 | #SBATCH --time=24:00:00 |
| | 60 | #SBATCH --ntasks=1 |
| | 61 | #SBATCH --cpus-per-task=1 |
| | 62 | #SBATCH --mem=256M |
| | 63 | #SBATCH --partition=centos7 |
| | 64 | #SBATCH --qos=normal |
| | 65 | ############################################################################### |
| | 66 | # Exit on error |
| | 67 | ############################################################################### |
| | 68 | set -euo pipefail |
| | 69 | ############################################################################### |
| | 70 | # Helpers |
| | 71 | ############################################################################### |
| | 72 | shopt -s expand_aliases |
| | 73 | alias dtstamp='date +%Y%m%d-%H:%M:%S' |
| | 74 | info(){ echo "Info[$(dtstamp)]: $*"; } |
| | 75 | get_ckpt_iter() { |
| | 76 | [[ -f "${CKPT_PATH}" ]] || { echo 0; return; } |
| | 77 | tr -cd '0-9' < "${CKPT_PATH}" 2>/dev/null || echo 0 |
| | 78 | } |
| | 79 | submit_worker() { |
| | 80 | sbatch "${RUNNER_SCRIPT}" | awk '{print $4}' |
| | 81 | } |
| | 82 | to_seconds() { |
| | 83 | # Acceptable time formats include "minutes", "minutes:seconds", "hours:min- utes:seconds", |
| | 84 | # "days-hours", "days-hours:minutes" and "days-hours:minutes:seconds" |
| | 85 | local input="$1" |
| | 86 | # Handle days-hours format by replacing '-' with ':' to unify the delimiter |
| | 87 | # Then split components into an array |
| | 88 | IFS=':' read -r -a parts <<< "${input//-/:}" |
| | 89 | case ${#parts[@]} in |
| | 90 | 1) # Format: minutes |
| | 91 | seconds=$((parts[0] * 60)) |
| | 92 | ;; |
| | 93 | 2) # Formats: minutes:seconds OR days-hours |
| | 94 | if [[ $input == *"-"* ]]; then |
| | 95 | seconds=$((parts[0] * 86400 + parts[1] * 3600)) |
| | 96 | else |
| | 97 | seconds=$((parts[0] * 60 + parts[1])) |
| | 98 | fi |
| | 99 | ;; |
| | 100 | 3) # Formats: hours:minutes:seconds OR days-hours:minutes |
| | 101 | if [[ $input == *"-"* ]]; then |
| | 102 | seconds=$((parts[0] * 86400 + parts[1] * 3600 + parts[2] * 60)) |
| | 103 | else |
| | 104 | seconds=$((parts[0] * 3600 + parts[1] * 60 + parts[2])) |
| | 105 | fi |
| | 106 | ;; |
| | 107 | 4) # Format: days-hours:minutes:seconds |
| | 108 | seconds=$((parts[0] * 86400 + parts[1] * 3600 + parts[2] * 60 + parts[3])) |
| | 109 | ;; |
| | 110 | *) |
| | 111 | echo "Unsupported format" |
| | 112 | exit 1 |
| | 113 | ;; |
| | 114 | esac |
| | 115 | echo "$seconds" |
| | 116 | } |
| | 117 | should_restart() { |
| | 118 | now=$(date +%s) |
| | 119 | elapsed=$(( now - restart_start_sec )) |
| | 120 | remaining=$(( restart_limit_sec - elapsed )) |
| | 121 | echo -ne "Time before restart: $remaining (sec)\r" >&2 |
| | 122 | (( remaining < SUBMITTER_MARGIN_SEC )) |
| | 123 | } |
| | 124 | ############################################################################### |
| | 125 | # Log settings |
| | 126 | ############################################################################### |
| | 127 | info "Start on $(hostname); JOBID=${SLURM_JOB_ID:-unknown}" |
| | 128 | info "Submitter settings:" |
| | 129 | info "CKPT_PATH=${CKPT_PATH}" |
| | 130 | info "MAX_ITER=${MAX_ITER}" |
| | 131 | info "RUNNER_SCRIPT=${RUNNER_SCRIPT}" |
| | 132 | info "SUBMITTER_MARGIN_SEC=${SUBMITTER_MARGIN_SEC}" |
| | 133 | info "SUBMITTER_TIME_LIMIT=${SUBMITTER_TIME_LIMIT}" |
| | 134 | ############################################################################### |
| | 135 | # Restore state if present |
| | 136 | ############################################################################### |
| | 137 | state_file="submitter_state.txt" |
| | 138 | submitter_cycle=0 |
| | 139 | if [[ -f "${state_file}" ]]; then |
| | 140 | source "${state_file}" |
| | 141 | info "Loaded state: submitter_cycle=${submitter_cycle}" |
| | 142 | else |
| | 143 | info "No previous state found; starting fresh." |
| | 144 | fi |
| | 145 | ############################################################################### |
| | 146 | # Track time for restart |
| | 147 | ############################################################################### |
| | 148 | restart_start_sec=$(date +%s) |
| | 149 | restart_limit_sec=$(to_seconds "${SUBMITTER_TIME_LIMIT}") |
| | 150 | ############################################################################### |
| | 151 | # Main Loop |
| | 152 | ############################################################################### |
| | 153 | info "Submitter cycle: ${submitter_cycle}" |
| | 154 | module load anaconda3/2023.07 # using python3 |
| | 155 | while true; do |
| | 156 | current_iter=$(get_ckpt_iter) |
| | 157 | info "Current checkpoint iteration: ${current_iter}" |
| | 158 | if (( current_iter >= MAX_ITER )); then |
| | 159 | info "Reached MAX_ITER=${MAX_ITER}; running final analytics." |
| | 160 | # List of analytics scripts to check |
| | 161 | analytics_scripts=( |
| | 162 | "aggregate_usage.py" |
| | 163 | "plot_progress.py" |
| | 164 | ) |
| | 165 | # Run resource usage aggregation |
| | 166 | if command -v python3 >/dev/null 2>&1; then |
| | 167 | # Loop over each script and run only if present |
| | 168 | for script in "${analytics_scripts[@]}"; do |
| | 169 | if [[ -f "${script}" ]]; then |
| | 170 | info "Running analytics ${script} ..." |
| | 171 | python3 "${script}" |
| | 172 | else |
| | 173 | info "WARNING: ${script} not found; skipping." |
| | 174 | fi |
| | 175 | done |
| | 176 | else |
| | 177 | info "WARNING: python3 not found; skipping analytics." |
| | 178 | fi |
| | 179 | info "Workflow complete. Exiting submitter." |
| | 180 | exit 0 |
| | 181 | |
| | 182 | fi |
| | 183 | JOBID=$(submit_worker) |
| | 184 | info "Submitted worker: ${JOBID}" |
| | 185 | # Log worker JobIDs for later aggregation |
| | 186 | id_log="worker_ids.txt" |
| | 187 | echo "${JOBID}" >> "${id_log}" |
| | 188 | # Wait for worker to finish |
| | 189 | info "Waiting on first of worker ${JOBID} to finish or self-restart." |
| | 190 | while squeue -j "${JOBID}" >/dev/null 2>&1; do |
| | 191 | sleep 10 |
| | 192 | if should_restart; then |
| | 193 | info "Submitter approaching timeout -> restarting self" |
| | 194 | # Save state |
| | 195 | echo "submitter_cycle=$((submitter_cycle+1))" > "${state_file}" |
| | 196 | # Submit a new submitter instance |
| | 197 | sbatch checkpoint_submitter.sh |
| | 198 | exit 0 |
| | 199 | |
| | 200 | fi |
| | 201 | done |
| | 202 | # Read worker exit code |
| | 203 | rc=$(sacct -j "${JOBID}" --format=ExitCode -P | awk -F: 'NR==2{print $1}') |
| | 204 | info "Worker finished with code ${rc}" |
| | 205 | case "${rc}" in |
| | 206 | 0) |
| | 207 | info "Worker reports completed workflow." |
| | 208 | exit 0 |
| | 209 | |
| | 210 | ;; |
| | 211 | 99|124) |
| | 212 | info "Checkpoint advance -> continuing workflow." |
| | 213 | ;; |
| | 214 | *) |
| | 215 | info "Unexpected worker failure (code ${rc}). Stopping." |
| | 216 | exit "${rc}" |
| | 217 | |
| | 218 | ;; |
| | 219 | esac |
| | 220 | # Loop continues to submit next worker |
| | 221 | done |
| | 222 | }}} |
| | 223 | |
| | 224 | == Checkpoint Runner == |
| | 225 | |
| | 226 | Here is the checkpoint runner job script for use with the following. |
| | 227 | |
| | 228 | * for jobs submitted by [wiki:Workshops/JobCheckpointing/Examples#CheckpointSubmitter Checkpoint Submitter] above |
| | 229 | * for running any of the following, minimal working examples of checkpointed applications in BASH, Python, or R. |
| | 230 | * For BASH, see [wiki:Workshops/JobCheckpointing/Examples#BASHCheckpointingExample BASH Checkpointint Example] |
| | 231 | * For Python, see [wiki:Workshops/JobCheckpointing/Examples#PythonCheckpointingExample Python Checkpointint Example] |
| | 232 | * For R, see [wiki:Workshops/JobCheckpointing/Examples#RCheckpointingExample R Checkpointint Example] |
| | 233 | |
| | 234 | === checkpoint_runner.sh === |
| | 235 | |
| | 236 | {{{ |
| | 237 | #!/bin/bash |
| | 238 | ############################################################################### |
| | 239 | # CHECKPOINT RUNNER: run application w/ checkpoint + controlled stop |
| | 240 | # Use with checkpoint_submitter.sh to re-submit as needed |
| | 241 | ############################################################################### |
| | 242 | # User settings with defaults |
| | 243 | ############################################################################### |
| | 244 | # APP_CMD : command to run (string) - must handle signal SIGTERM |
| | 245 | # CHECKPOINT_EVERY : checkpoint after every this many application iterations |
| | 246 | # CKPT_PATH : path to checkpoint file |
| | 247 | # LAUNCH_MODE : run app command directly or via srun |
| | 248 | # MAX_ITER : stop application after this many iterations total |
| | 249 | # MODULE_LIST : modules loaded before running app command |
| | 250 | # RUNNER_MARGIN_SEC: seconds before wall time to checkpoint (for timeout) |
| | 251 | # RUNNER_TIME_LIMIT: wall time <= 24 hours (format as --time in man sbatch) |
| | 252 | # SRUN_ARGS : arguments to srun, if needed |
| | 253 | ############################################################################### |
| | 254 | # Edit settings below |
| | 255 | # Or pass values via... |
| | 256 | # sbatch --export=ALL,Var1=...,...,VarN=... checkpoint_submitter.sh |
| | 257 | # Or pass values via... |
| | 258 | # Var1=... VarN=... sbatch checkpoint_submitter.sh |
| | 259 | ############################################################################### |
| | 260 | APP_CMD="${APP_CMD:-python3 checkpoint_signal_iter.py}" |
| | 261 | CHECKPOINT_EVERY="${CHECKPOINT_EVERY:-20}" |
| | 262 | CKPT_PATH="${CKPT_PATH:-state_iter.txt}" |
| | 263 | LAUNCH_MODE="${LAUNCH_MODE:-direct}" |
| | 264 | MAX_ITER="${MAX_ITER:-500}" |
| 66 | | |
| 67 | | info "Start on $(hostname); JOB_ID=${SLURM_JOB_ID}; RESTARTS=${SLURM_RESTART_COUNT:-0}" |
| 68 | | info "Settings:" |
| | 291 | get_ckpt_iter() { |
| | 292 | [[ -f "${CKPT_PATH}" ]] || { echo 0; return; } |
| | 293 | tr -cd '0-9' < "${CKPT_PATH}" 2>/dev/null || echo 0 |
| | 294 | } |
| | 295 | to_seconds() { |
| | 296 | # Acceptable time formats include "minutes", "minutes:seconds", "hours:min- utes:seconds", |
| | 297 | # "days-hours", "days-hours:minutes" and "days-hours:minutes:seconds" |
| | 298 | local input="$1" |
| | 299 | # Handle days-hours format by replacing '-' with ':' to unify the delimiter |
| | 300 | # Then split components into an array |
| | 301 | IFS=':' read -r -a parts <<< "${input//-/:}" |
| | 302 | case ${#parts[@]} in |
| | 303 | 1) # Format: minutes |
| | 304 | seconds=$((parts[0] * 60)) |
| | 305 | ;; |
| | 306 | 2) # Formats: minutes:seconds OR days-hours |
| | 307 | if [[ $input == *"-"* ]]; then |
| | 308 | seconds=$((parts[0] * 86400 + parts[1] * 3600)) |
| | 309 | else |
| | 310 | seconds=$((parts[0] * 60 + parts[1])) |
| | 311 | fi |
| | 312 | ;; |
| | 313 | 3) # Formats: hours:minutes:seconds OR days-hours:minutes |
| | 314 | if [[ $input == *"-"* ]]; then |
| | 315 | seconds=$((parts[0] * 86400 + parts[1] * 3600 + parts[2] * 60)) |
| | 316 | else |
| | 317 | seconds=$((parts[0] * 3600 + parts[1] * 60 + parts[2])) |
| | 318 | fi |
| | 319 | ;; |
| | 320 | 4) # Format: days-hours:minutes:seconds |
| | 321 | seconds=$((parts[0] * 86400 + parts[1] * 3600 + parts[2] * 60 + parts[3])) |
| | 322 | ;; |
| | 323 | *) |
| | 324 | echo "Unsupported format" |
| | 325 | exit 1 |
| | 326 | ;; |
| | 327 | esac |
| | 328 | echo "$seconds" |
| | 329 | } |
| | 330 | ############################################################################### |
| | 331 | # Log settings |
| | 332 | ############################################################################### |
| | 333 | info "Start on $(hostname); JOBID=${SLURM_JOB_ID:-unknown}" |
| | 334 | info "Runner settings:" |
| | 335 | info "APP_CMD=${APP_CMD}" |
| | 336 | info "CHECKPOINT_EVERY=${CHECKPOINT_EVERY}" |
| | 337 | info "CKPT_PATH=${CKPT_PATH}" |
| | 338 | info "LAUNCH_MODE=${LAUNCH_MODE}" |
| | 339 | info "MAX_ITER=${MAX_ITER}" |
| 73 | | info "TIME_LIMIT=${TIME_LIMIT}" |
| 74 | | info "MARGIN_SEC=${MARGIN_SEC}" |
| 75 | | info "CKPT_PATH=${CKPT_PATH}" |
| 76 | | info "CHECKPOINT_EVERY=${CHECKPOINT_EVERY}" |
| 77 | | info "MAX_ITER=${MAX_ITER}" |
| 78 | | info "MAX_RESTARTS=${MAX_RESTARTS}" |
| 79 | | |
| 80 | | # Load site modules (available on Cypress workers) |
| 81 | | module load slurm 2>/dev/null || true # makes scontrol visible on worker |
| 82 | | module load "${MODULE_LIST}" || true |
| 83 | | |
| 84 | | # Tool paths |
| 85 | | SCTRL="$(command -v scontrol || true)" |
| 86 | | |
| 87 | | # Short diagnostics |
| 88 | | if [[ -n "${SCTRL}" ]]; then |
| 89 | | echo "=== BEGIN JOB SNAPSHOT (scontrol) ===" |
| 90 | | "${SCTRL}" show job "${SLURM_JOB_ID}" | \ |
| 91 | | grep -E "JobId=|Partition=|QOS=|TimeLimit=|StartTime=|EndTime=|RunTime=|State=|Restarts=" |
| 92 | | echo "=== END JOB SNAPSHOT (scontrol) ===" |
| 93 | | else |
| 94 | | info "WARNING: scontrol not found on this node; in-place requeue will not be attempted." |
| 95 | | fi |
| 96 | | |
| 97 | | # Helpers |
| 98 | | get_ckpt_iter() { |
| 99 | | # reads the first numeric token from CKPT_PATH; returns 0 on any error |
| 100 | | [[ -f "${CKPT_PATH}" ]] || { echo 0; return; } |
| 101 | | local v |
| 102 | | v=$(tr -cd '0-9' < "${CKPT_PATH}" 2>/dev/null) |
| 103 | | echo "${v:-0}" |
| 104 | | } |
| 105 | | |
| 106 | | get_restarts() { |
| 107 | | if [[ -n "${SLURM_RESTART_COUNT:-}" ]]; then |
| 108 | | echo "${SLURM_RESTART_COUNT}" |
| 109 | | elif [[ -n "${SCTRL}" ]]; then |
| 110 | | "${SCTRL}" show job "${SLURM_JOB_ID}" | tr ' ' '\n' | awk -F= '/^Restarts=/{print $2; exit}' |
| 111 | | else |
| 112 | | echo "0" |
| 113 | | fi |
| 114 | | } |
| 115 | | |
| 116 | | to_seconds() { |
| 117 | | local t="$1" |
| 118 | | if [[ "$t" == *-*:*:* ]]; then |
| 119 | | local d h m s; IFS='-:' read -r d h m s <<<"$t" |
| 120 | | echo $(( d*86400 + h*3600 + m*60 + s )) |
| 121 | | else |
| 122 | | local h m s; IFS=':' read -r h m s <<<"$t" |
| 123 | | h=${h:-0}; m=${m:-0}; s=${s:-0} |
| 124 | | echo $(( h*3600 + m*60 + s )) |
| 125 | | fi |
| 126 | | } |
| 127 | | |
| 128 | | # Trap (batch shell) |
| 129 | | signal_handler () { |
| 130 | | info "TERM/INT caught in batch shell" |
| 131 | | local rc_local=0 |
| 132 | | |
| 133 | | if [[ -n "${child_pid:-}" ]]; then |
| 134 | | # srun or bash are group leaders; forward to their process group |
| 135 | | local child_pgid |
| 136 | | child_pgid="$(ps -o pgid= -p "${child_pid}" 2>/dev/null | awk '{print $1}')" |
| 137 | | if [[ -n "${child_pgid}" ]]; then |
| 138 | | kill -TERM "-${child_pgid}" 2>/dev/null || true |
| 139 | | else |
| 140 | | kill -TERM "${child_pid}" 2>/dev/null || true |
| 141 | | fi |
| 142 | | wait "${child_pid}" || rc_local=$? |
| 143 | | fi |
| 144 | | |
| 145 | | info "Program exit code (from trap): ${rc_local}" |
| 146 | | local restarts; restarts=$(get_restarts) |
| 147 | | if [[ ${rc_local} -eq 99 && ${requeued:-0} -eq 0 && ${restarts} -lt ${MAX_RESTARTS} && -n "${SCTRL}" ]]; then |
| 148 | | requeued=1 |
| 149 | | info "Checkpoint written (trap path). Requeueing in-place (same JobID)..." |
| 150 | | "${SCTRL}" requeue "${SLURM_JOB_ID}" || true |
| 151 | | info "Requeued via scontrol." |
| 152 | | fi |
| 153 | | exit 0 |
| 154 | | } |
| 155 | | trap 'signal_handler' TERM INT |
| 156 | | |
| 157 | | # Launch via timeout |
| 158 | | TOTAL_SEC=$(to_seconds "${TIME_LIMIT}") |
| 159 | | RUN_WINDOW_SEC=$(( TOTAL_SEC - MARGIN_SEC )) |
| | 344 | ############################################################################### |
| | 345 | # Load requested modules |
| | 346 | ############################################################################### |
| | 347 | info "Loading modules." |
| | 348 | module load "${MODULE_LIST}" |
| | 349 | ############################################################################### |
| | 350 | # Timeout window |
| | 351 | ############################################################################### |
| | 352 | TOTAL_SEC=$(to_seconds "${RUNNER_TIME_LIMIT}") |
| | 353 | RUN_WINDOW_SEC=$(( TOTAL_SEC - RUNNER_MARGIN_SEC )) |