#!/usr/bin/env bash # # Train the no-extra-information T/O/S disease-history ablation. # # Fixed model: # TrajMixer + all_future + relative + Weibull # # History modes: # timed (T): disease identities, order, and real first-onset times # ordered (O): disease identities and chronological order only # set (S): unordered disease set only # # Jobs assigned to one GPU run sequentially. Different GPUs run in parallel. # # Examples: # bash train_disease_history_ablation_linux.sh --gpus 0,1,2 # bash train_disease_history_ablation_linux.sh --gpus 0 --seeds 42 --modes ordered,set # bash train_disease_history_ablation_linux.sh --gpus 0,1 --dry-run # set -uo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" GPU_CSV="" SEED_CSV="42,43,44" MODE_CSV="timed,ordered,set" NUM_WORKERS=4 BATCH_SIZE=256 PYTHON_BIN="${PYTHON_BIN:-python}" CAMPAIGN_NAME="disease_history_ablation_no_extra" DRY_RUN=0 EXTRA_INFO_TYPES_FILE="$SCRIPT_DIR/extra_info_types_none.txt" ENTRYPOINT="$SCRIPT_DIR/train_all_future.py" usage() { cat <<'EOF' Usage: bash train_disease_history_ablation_linux.sh --gpus GPU_LIST [options] Required: --gpus LIST Comma-separated GPU ids, for example 0,1,2. Options: --seeds LIST Comma-separated seeds (default: 42,43,44). --modes LIST Subset of timed,ordered,set (default: all three). --batch-size N Batch size per task (default: 256). --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 commands without creating files or training. -h, --help Show this help message. Fixed experiment settings: architecture traj_mixer_v5 target all_future time mode relative distribution weibull extra information extra_info_types_none.txt Outputs: runs//seed_/traj_mixer_v5/... 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 ;; --seeds) [[ $# -ge 2 ]] || { echo "ERROR: --seeds requires a value." >&2 exit 2 } SEED_CSV="$2" shift 2 ;; --modes) [[ $# -ge 2 ]] || { echo "ERROR: --modes requires a value." >&2 exit 2 } MODE_CSV="$2" shift 2 ;; --batch-size) [[ $# -ge 2 ]] || { echo "ERROR: --batch-size requires a value." >&2 exit 2 } BATCH_SIZE="$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 } [[ -n "$SEED_CSV" ]] || { echo "ERROR: --seeds must not be empty." >&2 exit 2 } [[ -n "$MODE_CSV" ]] || { echo "ERROR: --modes must not be empty." >&2 exit 2 } [[ "$BATCH_SIZE" =~ ^[1-9][0-9]*$ ]] || { echo "ERROR: --batch-size must be a positive 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 } [[ -f "$ENTRYPOINT" ]] || { echo "ERROR: missing training entrypoint: $ENTRYPOINT" >&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 IFS=',' read -r -a SEEDS <<< "$SEED_CSV" declare -A SEEN_SEEDS=() for seed in "${SEEDS[@]}"; do [[ "$seed" =~ ^[0-9]+$ ]] || { echo "ERROR: invalid seed: $seed" >&2 exit 2 } [[ -z "${SEEN_SEEDS[$seed]+x}" ]] || { echo "ERROR: duplicate seed: $seed" >&2 exit 2 } SEEN_SEEDS["$seed"]=1 done IFS=',' read -r -a MODES <<< "$MODE_CSV" declare -A SEEN_MODES=() for mode in "${MODES[@]}"; do case "$mode" in timed|ordered|set) ;; *) echo "ERROR: invalid mode: $mode (expected timed, ordered, or set)." >&2 exit 2 ;; esac [[ -z "${SEEN_MODES[$mode]+x}" ]] || { echo "ERROR: duplicate mode: $mode" >&2 exit 2 } SEEN_MODES["$mode"]=1 done RUNS_ROOT="$SCRIPT_DIR/runs/$CAMPAIGN_NAME" LOG_ROOT="$SCRIPT_DIR/batch_logs/$CAMPAIGN_NAME" if ((!DRY_RUN)); then mkdir -p "$RUNS_ROOT" "$LOG_ROOT" fi declare -a JOB_SEEDS=() declare -a JOB_MODES=() for seed in "${SEEDS[@]}"; do for mode in "${MODES[@]}"; do JOB_SEEDS+=("$seed") JOB_MODES+=("$mode") done done print_command() { printf '%q ' "$@" printf '\n' } run_job() { local job_index="$1" local gpu="$2" local seed="${JOB_SEEDS[$job_index]}" local mode="${JOB_MODES[$job_index]}" local seed_runs_root="$RUNS_ROOT/seed_$seed" local seed_log_root="$LOG_ROOT/seed_$seed" local log_file="$seed_log_root/$mode.log" local -a command=( "$PYTHON_BIN" -u "$ENTRYPOINT" --runs_root "$seed_runs_root" --seed "$seed" --batch_size "$BATCH_SIZE" --num_workers "$NUM_WORKERS" --device cuda --model_architecture traj_mixer_v5 --time_mode relative --dist_mode weibull --extra_info_types_file "$EXTRA_INFO_TYPES_FILE" --disease_history_mode "$mode" ) if ((!DRY_RUN)); then mkdir -p "$seed_runs_root" "$seed_log_root" fi echo "[$(date '+%F %T')] START seed=$seed mode=$mode 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 seed=$seed mode=$mode gpu=$gpu" return 0 else local exit_code=$? echo "[$(date '+%F %T')] FAIL seed=$seed mode=$mode 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_SEEDS[@]}; job_index += ${#GPU_IDS[@]})); do run_job "$job_index" "$gpu" || failed=1 done return "$failed" } echo "Campaign: $CAMPAIGN_NAME" echo "Seeds: ${SEEDS[*]}" echo "Modes: ${MODES[*]}" echo "GPUs: ${GPU_IDS[*]}" echo "Configurations per seed: ${#MODES[@]}" echo "Total tasks: ${#JOB_SEEDS[@]}" 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 disease-history ablation tasks completed successfully." fi