2026-07-30 17:21:57 +08:00
|
|
|
#!/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
|
2026-07-30 18:29:15 +08:00
|
|
|
NUM_WORKERS_CALIBRATION=0
|
2026-07-30 17:21:57 +08:00
|
|
|
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).
|
2026-07-30 18:29:15 +08:00
|
|
|
--num-workers-calibration N CPU calibration workers per job. Default: 0,
|
|
|
|
|
which divides all logical CPUs across GPUs.
|
2026-07-30 17:21:57 +08:00
|
|
|
--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
|
|
|
|
|
;;
|
2026-07-30 18:29:15 +08:00
|
|
|
--num-workers-calibration)
|
|
|
|
|
[[ $# -ge 2 ]] || {
|
|
|
|
|
echo "ERROR: --num-workers-calibration requires a value." >&2
|
|
|
|
|
exit 2
|
|
|
|
|
}
|
|
|
|
|
NUM_WORKERS_CALIBRATION="$2"
|
|
|
|
|
shift 2
|
|
|
|
|
;;
|
2026-07-30 17:21:57 +08:00
|
|
|
--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
|
|
|
|
|
}
|
2026-07-30 18:29:15 +08:00
|
|
|
[[ "$NUM_WORKERS_CALIBRATION" =~ ^[0-9]+$ ]] || {
|
|
|
|
|
echo "ERROR: --num-workers-calibration must be a non-negative integer." >&2
|
|
|
|
|
exit 2
|
|
|
|
|
}
|
2026-07-30 17:21:57 +08:00
|
|
|
[[ "$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
|
|
|
|
|
|
2026-07-30 18:29:15 +08:00
|
|
|
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
|
|
|
|
|
|
2026-07-30 17:21:57 +08:00
|
|
|
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"
|
2026-07-30 18:29:15 +08:00
|
|
|
--num_workers_calibration "$NUM_WORKERS_CALIBRATION"
|
2026-07-30 17:21:57 +08:00
|
|
|
--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[*]}"
|
2026-07-30 18:29:15 +08:00
|
|
|
echo "Calibration CPU workers per GPU job: $NUM_WORKERS_CALIBRATION"
|
2026-07-30 17:21:57 +08:00
|
|
|
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
|