The target machine's card is busy with someone else's job, so a single end-to-end script stalls on work that does not actually need a GPU. Compiling the CUDA extensions needs nvcc, not a device, and downloading 18 GB of data needs neither. Those are the slow parts (~50 min + ~30 min), so phase A now runs entirely without the card: run_setup.sh bootstrap, conda, extensions, patches, data no GPU run_train.sh voxel_max measurement, training, evaluation GPU run_setup reports the GPU but never fails on it, and verify_env.py gained SKIP_CUDA_CHECK so import coverage still runs when no device is visible. TORCH_CUDA_ARCH_LIST is stated rather than probed, since the card may be unavailable at build time. run_train waits for the GPU instead of failing when it is busy: it polls until enough VRAM frees up (12h default), so it can be queued ahead of time. Past the deadline it proceeds anyway and lets the measured voxel_max adapt to whatever is actually free. keepalive.sh now takes the phase to supervise. Replaces run_all.sh and RUN.md with SETUP.md and TRAIN.md. Adds selfcheck.sh, which syntax-checks every script and flags CRLF endings - a shell script with either fails at its first line, which for an unattended weekend run means losing the weekend. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
289 lines
9.2 KiB
Bash
289 lines
9.2 KiB
Bash
#!/usr/bin/env bash
|
|
# SUM Parts - PHASE B: the GPU work
|
|
#
|
|
# Assumes run_setup.sh already finished (it checks for the marker). Everything
|
|
# here needs the card to itself:
|
|
#
|
|
# preflight -> choose voxel_max (measured) -> train -> evaluate
|
|
#
|
|
# Waits for the GPU rather than failing when it is busy: if another job holds
|
|
# the card, this parks until enough VRAM frees up. So it can be started ahead
|
|
# of time and left alone.
|
|
#
|
|
# Usage:
|
|
# bash scripts/run_train.sh
|
|
# bash scripts/run_train.sh --detach
|
|
# RUN_DRYRUN=1 bash scripts/run_train.sh # preflight only
|
|
set -uo pipefail
|
|
|
|
SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SETUP_MARKER="$HOME/sum-parts/runs/setup/SETUP_DONE"
|
|
OUT="$HOME/sum-parts/runs/train"
|
|
LOG="$OUT/train_phase.log"
|
|
STATUS="$OUT/STATUS"
|
|
DONE_MARKER="$OUT/TRAIN_DONE"
|
|
|
|
CFG="${CFG:-pointvector-xl}"
|
|
EPOCHS="${EPOCHS:-100}"
|
|
VAL_FREQ="${VAL_FREQ:-5}"
|
|
# tried high to low; first one that actually fits in VRAM wins.
|
|
# 64000 is the paper setting and needs ~16.5 GB.
|
|
VOXEL_CANDIDATES="${VOXEL_CANDIDATES:-64000 48000 40000 32000 24000}"
|
|
|
|
# how long to wait for someone else's job to release the card
|
|
GPU_WAIT_MINUTES="${GPU_WAIT_MINUTES:-720}"
|
|
GPU_FREE_MB="${GPU_FREE_MB:-16000}"
|
|
|
|
if [ "${1:-}" = "--detach" ]; then
|
|
mkdir -p "$OUT"
|
|
echo "detaching; log: $LOG"
|
|
setsid nohup bash "${BASH_SOURCE[0]}" > "$OUT/nohup.out" 2>&1 < /dev/null &
|
|
sleep 2
|
|
pgrep -af "run_train.sh" | grep -v detach || true
|
|
exit 0
|
|
fi
|
|
|
|
mkdir -p "$OUT"
|
|
exec > >(tee -a "$LOG") 2>&1
|
|
|
|
PHASE="starting"
|
|
STARTED=$(date '+%F %T')
|
|
VOXEL_MAX=""
|
|
|
|
say() { echo "[$(date '+%F %T')] $*"; }
|
|
head_() { echo; echo "════ $* ════"; }
|
|
|
|
write_status() {
|
|
{
|
|
echo "state : $1"
|
|
echo "phase : $PHASE"
|
|
echo "cfg : $CFG"
|
|
echo "voxel_max: ${VOXEL_MAX:-(not chosen yet)}"
|
|
echo "started : $STARTED"
|
|
echo "updated : $(date '+%F %T')"
|
|
[ -n "${EXTRA:-}" ] && echo "note : $EXTRA"
|
|
echo "log : $LOG"
|
|
} > "$STATUS"
|
|
}
|
|
|
|
die() {
|
|
say "GIVING UP in phase '$PHASE': $*"
|
|
EXTRA="$*" write_status "FAILED"
|
|
exit 1
|
|
}
|
|
|
|
STEP_RETRIES="${STEP_RETRIES:-4}"
|
|
STEP_BACKOFF="${STEP_BACKOFF:-60}"
|
|
|
|
step() {
|
|
PHASE="$1"; shift
|
|
head_ "$PHASE"
|
|
write_status "running"
|
|
local attempt=1 wait=$STEP_BACKOFF
|
|
while :; do
|
|
if "$@"; then
|
|
[ "$attempt" -gt 1 ] && say "phase '$PHASE' succeeded on attempt $attempt"
|
|
return 0
|
|
fi
|
|
[ "$attempt" -ge "$STEP_RETRIES" ] && die "$* (failed $attempt times)"
|
|
say "phase '$PHASE' failed (attempt $attempt/$STEP_RETRIES); retrying in ${wait}s"
|
|
EXTRA="retrying $PHASE ($attempt/$STEP_RETRIES)" write_status "retrying"
|
|
sleep "$wait"
|
|
attempt=$((attempt + 1)); wait=$((wait * 2))
|
|
done
|
|
}
|
|
|
|
step_once() {
|
|
PHASE="$1"; shift
|
|
head_ "$PHASE"
|
|
write_status "running"
|
|
"$@" || die "$*"
|
|
}
|
|
|
|
# ---------------------------------------------------------------- preflight
|
|
|
|
preflight() {
|
|
local fail=0
|
|
|
|
say "checking phase A completed"
|
|
if [ -f "$SETUP_MARKER" ]; then
|
|
say " marker present: $SETUP_MARKER"
|
|
else
|
|
say " MISSING: $SETUP_MARKER"
|
|
say " run phase A first: bash scripts/run_setup.sh"
|
|
fail=1
|
|
fi
|
|
|
|
say "checking environment"
|
|
if [ -x "$HOME/miniconda3/bin/conda" ]; then
|
|
source "$HOME/miniconda3/etc/profile.d/conda.sh"
|
|
if conda env list | grep -q '^sumparts '; then
|
|
say " conda env sumparts present"
|
|
else
|
|
say " MISSING: conda env 'sumparts'"; fail=1
|
|
fi
|
|
else
|
|
say " MISSING: miniconda"; fail=1
|
|
fi
|
|
|
|
say "checking data"
|
|
local d="$HOME/sum-parts/data/face_labeling/texsp_pcl"
|
|
if [ -d "$d/train" ]; then
|
|
say " train/val/test = $(find -L "$d/train" -name '*.ply' | wc -l)/$(find -L "$d/val" -name '*.ply' 2>/dev/null | wc -l)/$(find -L "$d/test" -name '*.ply' | wc -l)"
|
|
else
|
|
say " MISSING: $d/train"; fail=1
|
|
fi
|
|
|
|
say "checking GPU driver"
|
|
if ! command -v nvidia-smi > /dev/null; then
|
|
say " MISSING: nvidia-smi -- no GPU visible, and this phase cannot run without one"
|
|
fail=1
|
|
else
|
|
nvidia-smi --query-gpu=name,memory.total,memory.used,utilization.gpu \
|
|
--format=csv,noheader | sed 's/^/ /'
|
|
fi
|
|
|
|
[ "$fail" -eq 0 ] || return 1
|
|
say "preflight OK"
|
|
}
|
|
|
|
# ------------------------------------------------------------ wait for GPU
|
|
|
|
wait_for_gpu() {
|
|
local deadline=$(( $(date +%s) + GPU_WAIT_MINUTES * 60 ))
|
|
local total used free
|
|
|
|
total=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits | head -1)
|
|
say "card has ${total} MiB; waiting until ${GPU_FREE_MB} MiB is free"
|
|
say "(will wait up to ${GPU_WAIT_MINUTES} minutes)"
|
|
|
|
while :; do
|
|
used=$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits | head -1)
|
|
free=$(( total - used ))
|
|
if [ "$free" -ge "$GPU_FREE_MB" ]; then
|
|
say " ${free} MiB free -- proceeding"
|
|
return 0
|
|
fi
|
|
if [ "$(date +%s)" -ge "$deadline" ]; then
|
|
say " timed out with only ${free} MiB free"
|
|
say " continuing anyway; voxel_max will be measured against what is actually available"
|
|
return 0
|
|
fi
|
|
EXTRA="waiting for GPU (${free} MiB free, need ${GPU_FREE_MB})" write_status "waiting"
|
|
say " ${free} MiB free, need ${GPU_FREE_MB} -- checking again in 5 min"
|
|
sleep 300
|
|
done
|
|
}
|
|
|
|
# ---------------------------------------------------------------- vram pick
|
|
|
|
pick_voxel_max() {
|
|
say "measuring which voxel_max fits (high to low, first fit wins)"
|
|
say "NOTE: on WSL2 an oversized value does not OOM -- the driver spills into"
|
|
say " host RAM and the run completes 25-100x slower. So this checks peak"
|
|
say " allocation instead of trusting that it 'worked'."
|
|
|
|
source "$HOME/miniconda3/etc/profile.d/conda.sh"
|
|
conda activate sumparts
|
|
export WANDB_MODE=disabled WANDB_SILENT=true CUDA_HOME="$CONDA_PREFIX"
|
|
export PYTORCH_CUDA_ALLOC_CONF="garbage_collection_threshold:0.7,max_split_size_mb:128"
|
|
|
|
cd "$HOME/sum-parts/semantic_segmentation/PointNeXt_bundle/examples/segmentation" \
|
|
|| return 1
|
|
|
|
local vm log line fits
|
|
for vm in $VOXEL_CANDIDATES; do
|
|
log="$OUT/vram_${vm}.log"
|
|
say " trying voxel_max=$vm"
|
|
python -u "$SCRIPTS/bench_models.py" --iters 4 --voxel-max "$vm" \
|
|
--cfgs "$CFG" > "$log" 2>&1
|
|
line=$(grep -aE "^${CFG} +[0-9]" "$log" | tail -1)
|
|
if [ -z "$line" ]; then
|
|
say " no result (OOM or error); see $log"
|
|
continue
|
|
fi
|
|
say " $(echo "$line" | awk '{print "peak", $5, " s/iter", $6}')"
|
|
fits=$(echo "$line" | grep -o 'yes$' || true)
|
|
if [ -n "$fits" ]; then
|
|
VOXEL_MAX="$vm"
|
|
say " chosen: voxel_max=$VOXEL_MAX"
|
|
return 0
|
|
fi
|
|
say " does not fit -- would spill to host RAM"
|
|
done
|
|
|
|
say " nothing fit; falling back to the smallest candidate"
|
|
VOXEL_MAX=$(echo "$VOXEL_CANDIDATES" | awk '{print $NF}')
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------- train
|
|
|
|
do_train() {
|
|
if [ -f "$DONE_MARKER" ]; then
|
|
say "training already completed (marker: $DONE_MARKER)"
|
|
return 0
|
|
fi
|
|
|
|
# train_watchdog only auto-discovers checkpoints written after it starts,
|
|
# so hand it the newest one explicitly or a rerun restarts from epoch 1.
|
|
local ckpt
|
|
ckpt=$(find "$HOME/sum-parts/semantic_segmentation/PointNeXt_bundle/examples/segmentation/log/sumv2_triangle" \
|
|
-name '*_ckpt_latest.pth' -printf '%T@ %p\n' 2>/dev/null \
|
|
| sort -rn | head -1 | cut -d' ' -f2-)
|
|
if [ -n "$ckpt" ]; then
|
|
say "resuming from $(basename "$ckpt")"
|
|
else
|
|
say "no checkpoint found; starting fresh"
|
|
fi
|
|
|
|
say "training $CFG for $EPOCHS epochs at voxel_max=$VOXEL_MAX"
|
|
CFG_VOXEL_MAX="$VOXEL_MAX" VAL_VOXEL_MAX="$VOXEL_MAX" \
|
|
EPOCHS="$EPOCHS" VAL_FREQ="$VAL_FREQ" MAX_RETRIES=8 \
|
|
RESUME_CKPT="$ckpt" \
|
|
bash "$SCRIPTS/train_watchdog.sh" "$CFG" || return 1
|
|
|
|
touch "$DONE_MARKER"
|
|
return 0
|
|
}
|
|
|
|
do_eval() {
|
|
bash "$SCRIPTS/final_eval.sh" || say "final_eval non-zero (test split is blind; expected)"
|
|
bash "$SCRIPTS/eval_coarse.sh" || say "eval_coarse non-zero"
|
|
return 0
|
|
}
|
|
|
|
# ---------------------------------------------------------------- run
|
|
|
|
say "run_train starting (phase B - GPU required)"
|
|
say "cfg=$CFG epochs=$EPOCHS"
|
|
say "log: $LOG"
|
|
write_status "running"
|
|
|
|
step_once "preflight" preflight
|
|
|
|
if [ -n "${RUN_DRYRUN:-}" ]; then
|
|
say "DRYRUN set -- preflight passed, stopping before the real work"
|
|
write_status "dryrun-ok"
|
|
exit 0
|
|
fi
|
|
|
|
step_once "wait for GPU" wait_for_gpu
|
|
step "choose voxel_max" pick_voxel_max
|
|
step "train" do_train
|
|
step "evaluate" do_eval
|
|
|
|
PHASE="done"
|
|
write_status "DONE"
|
|
|
|
head_ "PHASE B COMPLETE"
|
|
say "cfg : $CFG"
|
|
say "voxel_max : $VOXEL_MAX"
|
|
grep -ahE 'Best ckpt' "$HOME/sum-parts/runs/"*/train.log 2>/dev/null | tail -2
|
|
if [ -f "$HOME/sum-parts/runs/coarse_eval/coarse.txt" ]; then
|
|
echo
|
|
echo "--- coarse (building / vegetation / vehicle / ground) ---"
|
|
cat "$HOME/sum-parts/runs/coarse_eval/coarse.txt"
|
|
fi
|
|
say "TRAIN PHASE DONE"
|