#!/usr/bin/env bash # # Recursively evaluate calibration for every completed all_future run. # # A runnable run contains: # - train_config.json with model_target_mode="all_future" # - best_model.pt # # calibration_evaluation_summary.json is written last by the evaluator and is # used as the completion marker. Existing non-empty markers are skipped unless # --force is supplied. next_token/Delphi2M runs are intentionally skipped. # # Jobs assigned to the same GPU run sequentially; different GPUs run in # parallel. # # Examples: # bash evaluate_calibration_all_runs_linux.sh --gpus 0 # bash evaluate_calibration_all_runs_linux.sh --gpus 0,1,2,3 # bash evaluate_calibration_all_runs_linux.sh --gpus 0,1 --dry-run # set -uo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" RUNS_ROOT="$SCRIPT_DIR/runs" LOG_ROOT="$SCRIPT_DIR/batch_logs/evaluate_calibration_all_runs" GPU_CSV="0" PYTHON_BIN="${PYTHON_BIN:-python}" NUM_WORKERS=4 NUM_WORKERS_CALIBRATION=0 BATCH_SIZE=128 DISEASE_CHUNK_SIZE=64 HORIZONS="" USE_AMP=0 FORCE=0 DRY_RUN=0 COMPLETION_FILE="calibration_evaluation_summary.json" EVALUATOR="$SCRIPT_DIR/evaluate_calibration.py" usage() { cat <<'EOF' Usage: bash evaluate_calibration_all_runs_linux.sh [options] Options: --gpus LIST Comma-separated GPU ids (default: 0). --runs-root PATH Root directory scanned recursively (default: ./runs). --log-root PATH Evaluation log root. --python PATH Python executable (default: $PYTHON_BIN or python). --num-workers N DataLoader workers per job (default: 4). --num-workers-calibration N CPU calibration workers per job. Default: 0, which divides all logical CPUs across GPUs. --batch-size N Evaluation batch size (default: 128). --disease-chunk-size N Disease projection chunk size (default: 64). --horizons LIST Optional comma-separated horizons in years. --use-amp Force CUDA automatic mixed precision. --force Recompute runs with an existing completion file. --dry-run Discover and print pending jobs only. -h, --help Show this help message. Only all_future runs are evaluated. next_token runs are skipped. Completion marker: calibration_evaluation_summary.json EOF } while (($# > 0)); do case "$1" in --gpus) [[ $# -ge 2 ]] || { echo "ERROR: --gpus requires a value." >&2 exit 2 } GPU_CSV="$2" shift 2 ;; --runs-root) [[ $# -ge 2 ]] || { echo "ERROR: --runs-root requires a value." >&2 exit 2 } RUNS_ROOT="$2" shift 2 ;; --log-root) [[ $# -ge 2 ]] || { echo "ERROR: --log-root requires a value." >&2 exit 2 } LOG_ROOT="$2" shift 2 ;; --python) [[ $# -ge 2 ]] || { echo "ERROR: --python requires a value." >&2 exit 2 } PYTHON_BIN="$2" shift 2 ;; --num-workers) [[ $# -ge 2 ]] || { echo "ERROR: --num-workers requires a value." >&2 exit 2 } NUM_WORKERS="$2" shift 2 ;; --num-workers-calibration) [[ $# -ge 2 ]] || { echo "ERROR: --num-workers-calibration requires a value." >&2 exit 2 } NUM_WORKERS_CALIBRATION="$2" shift 2 ;; --batch-size) [[ $# -ge 2 ]] || { echo "ERROR: --batch-size requires a value." >&2 exit 2 } BATCH_SIZE="$2" shift 2 ;; --disease-chunk-size) [[ $# -ge 2 ]] || { echo "ERROR: --disease-chunk-size requires a value." >&2 exit 2 } DISEASE_CHUNK_SIZE="$2" shift 2 ;; --horizons) [[ $# -ge 2 ]] || { echo "ERROR: --horizons requires a value." >&2 exit 2 } HORIZONS="$2" shift 2 ;; --use-amp) USE_AMP=1 shift ;; --force) FORCE=1 shift ;; --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 must not be empty." >&2 exit 2 } [[ "$NUM_WORKERS" =~ ^[0-9]+$ ]] || { echo "ERROR: --num-workers must be a non-negative integer." >&2 exit 2 } [[ "$NUM_WORKERS_CALIBRATION" =~ ^[0-9]+$ ]] || { echo "ERROR: --num-workers-calibration must be a non-negative integer." >&2 exit 2 } [[ "$BATCH_SIZE" =~ ^[1-9][0-9]*$ ]] || { echo "ERROR: --batch-size must be a positive integer." >&2 exit 2 } [[ "$DISEASE_CHUNK_SIZE" =~ ^[1-9][0-9]*$ ]] || { echo "ERROR: --disease-chunk-size must be a positive integer." >&2 exit 2 } [[ -d "$RUNS_ROOT" ]] || { echo "ERROR: runs root does not exist: $RUNS_ROOT" >&2 exit 2 } [[ -f "$EVALUATOR" ]] || { echo "ERROR: missing evaluator: $EVALUATOR" >&2 exit 2 } command -v "$PYTHON_BIN" >/dev/null 2>&1 || { echo "ERROR: Python executable not found: $PYTHON_BIN" >&2 exit 2 } RUNS_ROOT="$(cd -- "$RUNS_ROOT" && pwd)" if [[ "$LOG_ROOT" != /* ]]; then LOG_ROOT="$SCRIPT_DIR/$LOG_ROOT" fi 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 if ((NUM_WORKERS_CALIBRATION == 0)); then TOTAL_CPUS="$(nproc)" NUM_WORKERS_CALIBRATION=$(( (TOTAL_CPUS + ${#GPU_IDS[@]} - 1) / ${#GPU_IDS[@]} )) if ((NUM_WORKERS_CALIBRATION < 1)); then NUM_WORKERS_CALIBRATION=1 fi fi declare -a JOB_RUN_DIRS=() declare -a JOB_LOG_FILES=() add_job() { local run_dir="$1" local relative_run if [[ "$run_dir" == "$RUNS_ROOT" ]]; then relative_run="_root" else relative_run="${run_dir#"$RUNS_ROOT"/}" fi JOB_RUN_DIRS+=("$run_dir") JOB_LOG_FILES+=("$LOG_ROOT/$relative_run/evaluate_calibration.log") } run_count=0 incomplete_count=0 next_token_count=0 invalid_config_count=0 existing_count=0 while IFS= read -r -d '' config_path; do run_dir="${config_path%/train_config.json}" ((run_count += 1)) if [[ ! -f "$run_dir/best_model.pt" ]]; then echo "[SKIP] Incomplete run without best_model.pt: $run_dir" ((incomplete_count += 1)) continue fi if ! target_mode="$( "$PYTHON_BIN" -c \ 'import json,sys; print(str(json.load(open(sys.argv[1], encoding="utf-8")).get("model_target_mode", "next_token")).lower())' \ "$config_path" )"; then echo "[SKIP] Invalid train_config.json: $config_path" >&2 ((invalid_config_count += 1)) continue fi if [[ "$target_mode" != "all_future" ]]; then echo "[SKIP] model_target_mode=$target_mode: $run_dir" ((next_token_count += 1)) continue fi if ((FORCE == 0)) && [[ -s "$run_dir/$COMPLETION_FILE" ]]; then ((existing_count += 1)) continue fi add_job "$run_dir" done < <(find "$RUNS_ROOT" -type f -name "train_config.json" -print0) if ((DRY_RUN == 0)); then mkdir -p "$LOG_ROOT" fi print_command() { printf '%q ' "$@" printf '\n' } run_job() { local job_index="$1" local gpu="$2" local run_dir="${JOB_RUN_DIRS[$job_index]}" local log_file="${JOB_LOG_FILES[$job_index]}" local -a command=( "$PYTHON_BIN" -u "$EVALUATOR" --run_path "$run_dir" --output_path "$run_dir" --eval_split test --device cuda --num_workers "$NUM_WORKERS" --num_workers_calibration "$NUM_WORKERS_CALIBRATION" --batch_size "$BATCH_SIZE" --disease_chunk_size "$DISEASE_CHUNK_SIZE" ) if [[ -n "$HORIZONS" ]]; then command+=(--horizons "$HORIZONS") fi if ((USE_AMP)); then command+=(--use_amp) fi if ((FORCE)); then command+=(--force) fi echo "[$(date '+%F %T')] START gpu=$gpu" echo " run=$run_dir" if ((DRY_RUN)); then printf ' CUDA_VISIBLE_DEVICES=%q ' "$gpu" print_command "${command[@]}" return 0 fi mkdir -p "$(dirname -- "$log_file")" if CUDA_VISIBLE_DEVICES="$gpu" PYTHONUNBUFFERED=1 \ "${command[@]}" >"$log_file" 2>&1; then if [[ -s "$run_dir/$COMPLETION_FILE" ]]; then echo "[$(date '+%F %T')] DONE gpu=$gpu" return 0 fi echo "[$(date '+%F %T')] FAIL gpu=$gpu" >&2 echo " Missing completion marker: $run_dir/$COMPLETION_FILE" >&2 echo " See: $log_file" >&2 return 1 else local exit_code=$? echo "[$(date '+%F %T')] FAIL 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_RUN_DIRS[@]}; job_index += ${#GPU_IDS[@]} )); do run_job "$job_index" "$gpu" || failed=1 done return "$failed" } echo "Runs root: $RUNS_ROOT" echo "GPUs: ${GPU_IDS[*]}" echo "Calibration CPU workers per GPU job: $NUM_WORKERS_CALIBRATION" echo "Runs discovered: $run_count" echo "Incomplete runs skipped: $incomplete_count" echo "next_token runs skipped: $next_token_count" echo "Invalid configs skipped: $invalid_config_count" echo "Existing calibration results skipped: $existing_count" echo "Pending all_future evaluations: ${#JOB_RUN_DIRS[@]}" echo "Log root: $LOG_ROOT" echo if ((${#JOB_RUN_DIRS[@]} == 0)); then echo "No pending all_future calibration evaluations." exit 0 fi 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 calibration evaluations failed." >&2 echo "Inspect logs under: $LOG_ROOT" >&2 exit 1 fi if ((DRY_RUN)); then echo "Dry run completed successfully." else echo "All pending all_future calibration evaluations completed." fi