FACT / scripts /train_full.sh
zhicao's picture
Add FACT training code
af8c481 verified
Raw History Blame Contribute Delete
6.23 kB
#!/bin/bash
set -euo pipefail
# FACT full training (from scratch, no LoRA) on a T-Rex-format LeRobot-v2.1
# dataset: offline caches (frozen Wan2.2 VAE latents sharded over all GPUs,
# umT5-XXL text embeddings on node 0), then torch.distributed DDP training with
# checkpoints every SAVE_STEPS, W&B scalars and a periodic monitoring video.
#
# Usage (inside tmux):
# [source your env] # optional: FACT_ENV_FILE=/path/to/env.sh
# bash scripts/train_full.sh [extra train_ddp.py args]
# Multi-node: run on every node with NNODES / NODE_RANK / MASTER_ADDR set
# (node rank 0 = MASTER_ADDR; caches are built cooperatively on shared storage).
# Env overrides: DATA_ROOT OUTPUT_DIR CACHE_DIR FACT_CONFIG WAN22_CKPT_DIR NUM_GPUS
# PER_DEVICE_BATCH LR MAX_STEPS SAVE_STEPS VIDEO_STEPS WANDB_PROJECT
# WANDB_RUN_NAME REQUIRE_FORCE(0/1)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DATA_ROOT="${DATA_ROOT:-${TREX_DATA_ROOT:-$REPO_ROOT/data/trex_full_force}}"
OUTPUT_DIR="${OUTPUT_DIR:-$REPO_ROOT/checkpoints/$(basename "$DATA_ROOT")_fact}"
CACHE_DIR="${CACHE_DIR:-${FACT_CACHE_DIR:-$REPO_ROOT/cache/$(basename "$DATA_ROOT")}}"
FACT_CONFIG="${FACT_CONFIG:-$REPO_ROOT/configs/fact_base_text.json}"
WAN22_CKPT_DIR="${WAN22_CKPT_DIR:-$REPO_ROOT/checkpoints/Wan2.2-TI2V-5B}"
NUM_GPUS="${NUM_GPUS:-16}"
NNODES="${NNODES:-1}"
NODE_RANK="${NODE_RANK:-0}"
MASTER_ADDR="${MASTER_ADDR:-127.0.0.1}"
MASTER_PORT="${MASTER_PORT:-29701}"
PER_DEVICE_BATCH="${PER_DEVICE_BATCH:-8}"
LR="${LR:-3e-4}"
MAX_STEPS="${MAX_STEPS:-100000}"
SAVE_STEPS="${SAVE_STEPS:-10000}"
VIDEO_STEPS="${VIDEO_STEPS:-1000}"
VIDEO_SAVE_LOCAL="${VIDEO_SAVE_LOCAL:-0}"
WANDB_PROJECT="${WANDB_PROJECT:-fact}"
WANDB_RUN_NAME="${WANDB_RUN_NAME:-$(basename "$OUTPUT_DIR")}"
REQUIRE_FORCE="${REQUIRE_FORCE:-0}" # 1 = dataset must carry the trex_track_force tactile schema/manifest
# ---- environment ------------------------------------------------------------
if [ -n "${FACT_ENV_FILE:-}" ] && [ -f "$FACT_ENV_FILE" ]; then
set +eu
# shellcheck disable=SC1090
source "$FACT_ENV_FILE"
set -eu
fi
export PYTHONPATH="$REPO_ROOT${PYTHONPATH:+:$PYTHONPATH}"
cd "$REPO_ROOT"
filt() { grep --line-buffered -v -E "libhggcrt1|ALINPU|^\s*$"; }
if [ ! -f "$DATA_ROOT/meta/info.json" ]; then
echo "ERROR: LeRobot-v2 dataset not found: $DATA_ROOT" >&2; exit 1
fi
for required in "$WAN22_CKPT_DIR/Wan2.2_VAE.pth" "$WAN22_CKPT_DIR/models_t5_umt5-xxl-enc-bf16.pth" \
"$WAN22_CKPT_DIR/google/umt5-xxl/tokenizer.json" "$FACT_CONFIG"; do
if [ ! -f "$required" ]; then echo "ERROR: missing file: $required" >&2; exit 1; fi
done
READY_ARGS=(--dataset-root "$DATA_ROOT")
if [ "$REQUIRE_FORCE" = "1" ]; then READY_ARGS+=(--require-force); fi
python "$REPO_ROOT/scripts/check_dataset_ready.py" "${READY_ARGS[@]}" 2>&1 | filt
# ---- offline caches ---------------------------------------------------------
N_EPISODES=$(python -c "import json;print(json.load(open('$DATA_ROOT/meta/info.json'))['total_episodes'])")
count_cached() { ls "$CACHE_DIR/latents" 2>/dev/null | grep -c '^episode_.*\.npy$' || true; }
TOTAL_SHARDS=$((NNODES * NUM_GPUS))
if [ "$(count_cached)" -lt "$N_EPISODES" ]; then
echo ">>> building VAE latent cache: $(count_cached)/$N_EPISODES episodes cached; node $NODE_RANK runs shards $((NODE_RANK * NUM_GPUS))..$((NODE_RANK * NUM_GPUS + NUM_GPUS - 1)) of $TOTAL_SHARDS"
mkdir -p "$CACHE_DIR/latents"
for ((i = 0; i < NUM_GPUS; i++)); do
SHARD=$((NODE_RANK * NUM_GPUS + i))
CUDA_VISIBLE_DEVICES=$i python scripts/build_cache_dataset.py --what latents \
--dataset-root "$DATA_ROOT" --out "$CACHE_DIR" \
--vae "$WAN22_CKPT_DIR/Wan2.2_VAE.pth" --shard "$SHARD" --num-shards "$TOTAL_SHARDS" \
2>&1 | filt | sed "s/^/[shard $SHARD] /" &
done
wait
fi
if [ "$NODE_RANK" = "0" ] && [ ! -f "$CACHE_DIR/text_cache.npz" ]; then
echo ">>> building umT5 text cache"
CUDA_VISIBLE_DEVICES=0 python scripts/build_cache_dataset.py --what text \
--dataset-root "$DATA_ROOT" --out "$CACHE_DIR" \
--text-encoder "$WAN22_CKPT_DIR/models_t5_umt5-xxl-enc-bf16.pth" \
--tokenizer "$WAN22_CKPT_DIR/google/umt5-xxl" --max-text-tokens 64 2>&1 | filt
fi
until [ -f "$CACHE_DIR/text_cache.npz" ] && [ "$(count_cached)" -ge "$N_EPISODES" ]; do
echo ">>> node $NODE_RANK waiting for caches: $(count_cached)/$N_EPISODES latents, text=$([ -f "$CACHE_DIR/text_cache.npz" ] && echo yes || echo no)"
sleep 60
done
# ---- W&B: online if credentials exist, otherwise offline --------------------
if [ -z "${WANDB_API_KEY:-}" ] && ! grep -qs "api.wandb.ai" "$HOME/.netrc"; then
echo ">>> no W&B credentials found; using WANDB_MODE=offline"
export WANDB_MODE="${WANDB_MODE:-offline}"
fi
mkdir -p "$OUTPUT_DIR"
ulimit -n 65536 2>/dev/null || true
echo ">>> FACT training: $NNODES node(s) x $NUM_GPUS GPUs x batch $PER_DEVICE_BATCH, $MAX_STEPS steps, save every $SAVE_STEPS -> $OUTPUT_DIR"
if [ "$NNODES" = "1" ]; then
LAUNCH=(python -m torch.distributed.run --nproc_per_node="$NUM_GPUS" --standalone)
else
LAUNCH=(python -m torch.distributed.run --nproc_per_node="$NUM_GPUS" --nnodes="$NNODES" --node_rank="$NODE_RANK" \
--master_addr="$MASTER_ADDR" --master_port="$MASTER_PORT")
fi
"${LAUNCH[@]}" \
"$REPO_ROOT/scripts/train_ddp.py" \
--dataset-root "$DATA_ROOT" \
--cache-dir "$CACHE_DIR" \
--output-dir "$OUTPUT_DIR" \
--config "$FACT_CONFIG" \
--blocks 4 \
--max-steps "$MAX_STEPS" \
--per-device-batch "$PER_DEVICE_BATCH" \
--lr "$LR" \
--min-lr-ratio 0.1 \
--warmup-steps 2000 \
--weight-decay 0.01 \
--clip 1.0 \
--seed 42 \
--workers 4 \
--bf16 1 \
--log-steps 20 \
--save-steps "$SAVE_STEPS" \
--save-total-limit 10 \
--resume auto \
--report-to wandb \
--wandb-project "$WANDB_PROJECT" \
--wandb-run-name "$WANDB_RUN_NAME" \
--video-steps "$VIDEO_STEPS" \
--video-episode 0 \
--video-num-chunks 4 \
--video-save-local "$VIDEO_SAVE_LOCAL" \
"$@" 2>&1 | filt | tee -a "$OUTPUT_DIR/train_node${NODE_RANK}.log"