File size: 6,032 Bytes
58258b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/usr/bin/env bash
# Copyright The Marin Authors
# SPDX-License-Identifier: Apache-2.0
#
# Self-healing launcher for train_sft_qwen3.py on ONE 8xH100 node.
#
#   bash run_sft.sh <exp_name> [num_train_steps]
#
# Loops torchrun: on a transient crash (e.g. an NCCL watchdog collective timeout)
# it waits 30s and relaunches with --resume auto (continues from checkpoints-temp)
# and a FIXED --wandb-id (continues the SAME W&B run instead of forking a new one).
# Exits 0 when training reaches the step target. Bails out if it fast-fails
# repeatedly -- that means a real error (bad config / OOM / disk), not a blip.
#
# Data must already be prepared at $DATA_ROOT/<exp_name> by prepare_sft_data.py.
# For a FRESH run, delete $RUNS_ROOT/<exp_name> first (else --resume auto continues
# the old checkpoints). Launch under nohup so it survives your shell:
#
#   nohup bash gpu-sft/scripts/gpu_sft/run_sft.sh science_depth4v3_nofilter \
#       > /opt/tiger/lmc_muon/gpu-sft-runs/science_depth4v3_nofilter/wrapper.log 2>&1 &
#
# NOTE: no `set -e` on purpose -- the whole point is to survive torchrun failures.
set -uo pipefail

EXP="${1:?usage: run_sft.sh <exp_name> [num_train_steps]}"
NUM_STEPS="${2:-2000}"
NPROC="${NPROC:-8}"

DATA_ROOT="${DATA_ROOT:-/opt/tiger/lmc_muon/gpu-sft-data}"
RUNS_ROOT="${RUNS_ROOT:-/opt/tiger/lmc_muon/gpu-sft-runs}"
REPO="${REPO:-/opt/tiger/lmc_muon/self-verified-distillation}"
WANDB_PROJECT="${WANDB_PROJECT:-marin-instill-gpu}"
WANDB_ID="${WANDB_ID:-$EXP}"   # override to FORCE a fresh W&B run id (avoids reattaching to a prior partial run of the same name)

# Durability: async HDFS mirror of hf/ exports + native checkpoints/. Local NVMe stays the
# training --out (fast, no FUSE I/O stall that could trip the NCCL watchdog); this only
# copies in the background so a machine kill can't erase progress. Set MIRROR=0 to disable.
MIRROR="${MIRROR:-1}"
HDFS_ROOT="${HDFS_ROOT:-/mnt/hdfs/fangzhao_writable/marin-sft}"
MIRROR_EVERY="${MIRROR_EVERY:-300}"

DATA="$DATA_ROOT/$EXP"
OUT="$RUNS_ROOT/$EXP"
LOG="$OUT/train.log"

MAX_RESTARTS="${MAX_RESTARTS:-30}"     # absolute cap on relaunches
FAST_FAIL_SEC="${FAST_FAIL_SEC:-180}"  # a run dying sooner than this = a hard fail
MAX_FAST_FAILS="${MAX_FAST_FAILS:-3}"  # bail after this many CONSECUTIVE fast fails

export HF_HOME="${HF_HOME:-/opt/tiger/lmc_muon/hf_cache}"
export TOKENIZERS_PARALLELISM=false
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
export WANDB_MODE="${WANDB_MODE:-online}"
export NCCL_DEBUG="${NCCL_DEBUG:-WARN}"

if [[ ! -d "$DATA" ]]; then
  echo "[run_sft] ERROR: prepared data not found at $DATA -- run prepare_sft_data.py first" >&2
  exit 2
fi
mkdir -p "$OUT"
cd "$REPO" || { echo "[run_sft] ERROR: cannot cd $REPO" >&2; exit 2; }

echo "[run_sft] exp=$EXP steps=$NUM_STEPS nproc=$NPROC wandb-id=$WANDB_ID out=$OUT" | tee -a "$LOG"

# --- durability: background HDFS mirror (hf/ exports + native checkpoints/; skip the
# rolling checkpoints-temp/ which is overwritten and not rsync-snapshot-safe) ----------
DEST="$HDFS_ROOT/$EXP"
mirror_once() { rsync -a --exclude 'checkpoints-temp' "$OUT/" "$DEST/" 2>>"$OUT/mirror.log"; }
mirror_pid=""; push_pid=""
if [ "$MIRROR" = "1" ] && [ -d "$(dirname "$HDFS_ROOT")" ]; then
  mkdir -p "$DEST"
  echo "[run_sft] HDFS mirror ON: $OUT -> $DEST every ${MIRROR_EVERY}s" | tee -a "$LOG"
  ( while true; do sleep "$MIRROR_EVERY"; mirror_once; done ) &
  mirror_pid=$!
elif [ -n "${PUSH_HF_REPO:-}" ]; then
  # No HDFS (e.g. A800 boxes): push each hf/step-N to a private HF repo instead.
  echo "[run_sft] no HDFS -> HF push: hf/step-N -> $PUSH_HF_REPO" | tee -a "$LOG"
  nohup python gpu-sft/scripts/gpu_sft/push_exports.py --out "$OUT" --repo "$PUSH_HF_REPO" >> "$OUT/push.log" 2>&1 &
  push_pid=$!
else
  echo "[run_sft] WARNING: NO durability -- no HDFS mount and no PUSH_HF_REPO; checkpoints on ephemeral disk only!" | tee -a "$LOG"
fi
trap '[ -n "${mirror_pid:-}" ] && kill "$mirror_pid" 2>/dev/null; [ -n "${push_pid:-}" ] && kill "$push_pid" 2>/dev/null' EXIT

fast_fails=0
for attempt in $(seq 1 "$MAX_RESTARTS"); do
  echo "[run_sft] === attempt $attempt/$MAX_RESTARTS  $(date -u) ===" | tee -a "$LOG"
  start=$(date +%s)

  torchrun --standalone --nproc_per_node="$NPROC" \
    gpu-sft/scripts/gpu_sft/train_sft_qwen3.py \
    --data "$DATA" --out "$OUT" --recipe qwen3-8b \
    --train-batch-size 64 --per-device-batch 1 --loss-groups 2 \
    --num-train-steps "$NUM_STEPS" \
    --steps-per-hf-export 100 --steps-per-checkpoint 500 \
    --loss-chunk-size 2048 --hf-export-dtype bfloat16 \
    --init-mode all_ranks --resume auto \
    --wandb-project "$WANDB_PROJECT" --wandb-name "$EXP" --wandb-id "$WANDB_ID" \
    --log-every 1 >> "$LOG" 2>&1
  rc=$?
  dur=$(( $(date +%s) - start ))

  if [[ $rc -eq 0 ]]; then
    echo "[run_sft] SUCCESS: $EXP finished (rc=0) after $attempt attempt(s)" | tee -a "$LOG"
    if [ -n "$mirror_pid" ]; then echo "[run_sft] final HDFS mirror..." | tee -a "$LOG"; mirror_once; fi
    if [ -n "$push_pid" ]; then
      echo "[run_sft] final HF push..." | tee -a "$LOG"
      kill "$push_pid" 2>/dev/null
      python gpu-sft/scripts/gpu_sft/push_exports.py --out "$OUT" --repo "$PUSH_HF_REPO" --once >> "$OUT/push.log" 2>&1
    fi
    exit 0
  fi

  echo "[run_sft] exit rc=$rc after ${dur}s" | tee -a "$LOG"
  if [[ $dur -lt $FAST_FAIL_SEC ]]; then
    fast_fails=$((fast_fails + 1))
    echo "[run_sft] fast-fail $fast_fails/$MAX_FAST_FAILS (died <${FAST_FAIL_SEC}s -> likely a real error, not transient)" | tee -a "$LOG"
    if [[ $fast_fails -ge $MAX_FAST_FAILS ]]; then
      echo "[run_sft] ABORT: $MAX_FAST_FAILS consecutive fast fails -- inspect $LOG (config/OOM/disk, not a blip)" | tee -a "$LOG"
      exit 1
    fi
  else
    fast_fails=0   # ran a while then died -> treat as transient, reset the counter
  fi

  echo "[run_sft] relaunching in 30s (will --resume auto from checkpoints-temp)..." | tee -a "$LOG"
  sleep 30
done

echo "[run_sft] ABORT: hit MAX_RESTARTS=$MAX_RESTARTS without finishing" | tee -a "$LOG"
exit 1