#!/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"