#!/usr/bin/env bash # # Run the complete DeepHealth training matrix on a Linux GPU server. # # The matrix contains: # 1. FFN + Delphi2M next-token reproduction # 2. FFN/TrajMixer x absolute/relative x exponential/Weibull # # Every task uses one GPU. Each selected GPU runs its assigned tasks # sequentially, while different GPUs run in parallel. # # Example: # bash train_batch_linux.sh --gpus 0,1,2,3 # set -uo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" GPU_CSV="" SEED=42 NUM_WORKERS=4 PYTHON_BIN="${PYTHON_BIN:-python}" CAMPAIGN_NAME="full_factorial_smoking_alcohol_bmi" DRY_RUN=0 BATCH_SIZE=256 EXTRA_INFO_TYPES_FILE="$SCRIPT_DIR/extra_info_types_smoking_alcohol_bmi.txt" usage() { cat <<'EOF' Usage: bash train_batch_linux.sh --gpus GPU_LIST [options] Required: --gpus LIST Comma-separated GPU ids, for example 0,1,2,3. Options: --seed N Training seed for every task (default: 42). --num-workers N DataLoader workers per task (default: 4). --python PATH Python executable (default: $PYTHON_BIN or python). --campaign NAME Output campaign name. --dry-run Print all commands without running them. -h, --help Show this help message. Fixed experiment settings: batch_size 256 extra_info_types extra_info_types_smoking_alcohol_bmi.txt continuous scaling required train-split RobustScale model size Defaults from the individual training entrypoints Outputs: runs///... batch_logs//seed_/.log EOF } while (($# > 0)); do case "$1" in --gpus) [[ $# -ge 2 ]] || { echo "ERROR: --gpus requires a value." >&2 exit 2 } GPU_CSV="$2" shift 2 ;; --seed) [[ $# -ge 2 ]] || { echo "ERROR: --seed requires a value." >&2 exit 2 } SEED="$2" shift 2 ;; --num-workers) [[ $# -ge 2 ]] || { echo "ERROR: --num-workers requires a value." >&2 exit 2 } NUM_WORKERS="$2" shift 2 ;; --python) [[ $# -ge 2 ]] || { echo "ERROR: --python requires a value." >&2 exit 2 } PYTHON_BIN="$2" shift 2 ;; --campaign) [[ $# -ge 2 ]] || { echo "ERROR: --campaign requires a value." >&2 exit 2 } CAMPAIGN_NAME="$2" shift 2 ;; --dry-run) DRY_RUN=1 shift ;; -h|--help) usage exit 0 ;; *) echo "ERROR: unknown argument: $1" >&2 usage >&2 exit 2 ;; esac done [[ -n "$GPU_CSV" ]] || { echo "ERROR: --gpus is required." >&2 usage >&2 exit 2 } [[ "$SEED" =~ ^[0-9]+$ ]] || { echo "ERROR: --seed must be a non-negative integer." >&2 exit 2 } [[ "$NUM_WORKERS" =~ ^[0-9]+$ ]] || { echo "ERROR: --num-workers must be a non-negative integer." >&2 exit 2 } [[ "$CAMPAIGN_NAME" =~ ^[A-Za-z0-9._-]+$ ]] || { echo "ERROR: --campaign may contain only letters, numbers, ., _, and -." >&2 exit 2 } [[ -f "$EXTRA_INFO_TYPES_FILE" ]] || { echo "ERROR: missing extra-info file: $EXTRA_INFO_TYPES_FILE" >&2 exit 2 } command -v "$PYTHON_BIN" >/dev/null 2>&1 || { echo "ERROR: Python executable not found: $PYTHON_BIN" >&2 exit 2 } IFS=',' read -r -a GPU_IDS <<< "$GPU_CSV" declare -A SEEN_GPUS=() for gpu in "${GPU_IDS[@]}"; do [[ -n "$gpu" && "$gpu" =~ ^[A-Za-z0-9._:-]+$ ]] || { echo "ERROR: invalid GPU id: $gpu" >&2 exit 2 } [[ -z "${SEEN_GPUS[$gpu]+x}" ]] || { echo "ERROR: duplicate GPU id: $gpu" >&2 exit 2 } SEEN_GPUS["$gpu"]=1 done RUNS_ROOT="$SCRIPT_DIR/runs/$CAMPAIGN_NAME" LOG_ROOT="$SCRIPT_DIR/batch_logs/$CAMPAIGN_NAME/seed_$SEED" mkdir -p "$RUNS_ROOT" "$LOG_ROOT" declare -a JOB_NAMES=() declare -a JOB_ENTRYPOINTS=() declare -a JOB_ARCHITECTURES=() declare -a JOB_TIME_MODES=() declare -a JOB_DIST_MODES=() add_job() { JOB_NAMES+=("$1") JOB_ENTRYPOINTS+=("$2") JOB_ARCHITECTURES+=("$3") JOB_TIME_MODES+=("$4") JOB_DIST_MODES+=("$5") } add_job \ "ffn_next_token_absolute_delphi2m" \ "train_next_step.py" \ "transformer_ffn_v1" \ "absolute" \ "exponential" for architecture in transformer_ffn_v1 traj_mixer_v5; do for time_mode in absolute relative; do for dist_mode in exponential weibull; do add_job \ "${architecture}_all_future_${time_mode}_${dist_mode}" \ "train_all_future.py" \ "$architecture" \ "$time_mode" \ "$dist_mode" done done done print_command() { printf '%q ' "$@" printf '\n' } run_job() { local job_index="$1" local gpu="$2" local job_name="${JOB_NAMES[$job_index]}" local entrypoint="${JOB_ENTRYPOINTS[$job_index]}" local architecture="${JOB_ARCHITECTURES[$job_index]}" local time_mode="${JOB_TIME_MODES[$job_index]}" local dist_mode="${JOB_DIST_MODES[$job_index]}" local log_file="$LOG_ROOT/$job_name.log" local -a command=( "$PYTHON_BIN" -u "$SCRIPT_DIR/$entrypoint" --runs_root "$RUNS_ROOT" --seed "$SEED" --batch_size "$BATCH_SIZE" --extra_info_types_file "$EXTRA_INFO_TYPES_FILE" --model_architecture "$architecture" --num_workers "$NUM_WORKERS" --device cuda ) if [[ "$entrypoint" == "train_all_future.py" ]]; then command+=( --time_mode "$time_mode" --dist_mode "$dist_mode" ) fi echo "[$(date '+%F %T')] START job=$job_name gpu=$gpu" echo " log=$log_file" if ((DRY_RUN)); then printf ' CUDA_VISIBLE_DEVICES=%q ' "$gpu" print_command "${command[@]}" return 0 fi if CUDA_VISIBLE_DEVICES="$gpu" PYTHONUNBUFFERED=1 \ "${command[@]}" >"$log_file" 2>&1; then echo "[$(date '+%F %T')] DONE job=$job_name gpu=$gpu" return 0 else local exit_code=$? echo "[$(date '+%F %T')] FAIL job=$job_name gpu=$gpu exit=$exit_code" >&2 echo " See: $log_file" >&2 return "$exit_code" fi } worker() { local slot="$1" local gpu="${GPU_IDS[$slot]}" local job_index local failed=0 for ((job_index = slot; job_index < ${#JOB_NAMES[@]}; job_index += ${#GPU_IDS[@]})); do run_job "$job_index" "$gpu" || failed=1 done return "$failed" } echo "Campaign: $CAMPAIGN_NAME" echo "Seed: $SEED" echo "GPUs: ${GPU_IDS[*]}" echo "Tasks: ${#JOB_NAMES[@]}" echo "Runs root: $RUNS_ROOT" echo "Log root: $LOG_ROOT" echo declare -a WORKER_PIDS=() for ((slot = 0; slot < ${#GPU_IDS[@]}; slot++)); do worker "$slot" & WORKER_PIDS+=("$!") done overall_status=0 for pid in "${WORKER_PIDS[@]}"; do wait "$pid" || overall_status=1 done if ((overall_status != 0)); then echo "One or more training tasks failed. Inspect logs under: $LOG_ROOT" >&2 exit 1 fi if ((DRY_RUN)); then echo "Dry run completed successfully." else echo "All training tasks completed successfully." fi