Files
elizaos--eliza/packages/training/scripts/checkpoint_sync_loop.sh
T
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

307 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# checkpoint_sync_loop.sh — pull intermediate Vast.ai training checkpoints
# back to the local box at a fixed cadence so a UI (or operator) can plot
# progress while the run is still going.
#
# Pairs with `eval_loop.sh` (which scores each pulled checkpoint) and
# `progress_report.py` (which renders an HTML chart from `_progress.jsonl`).
#
# This script does NOT spin up Vast instances — that is owned by
# `train_vast.sh`. We only read the instance id it already provisioned
# (via `ELIZA_VAST_INSTANCE_ID` / `VAST_INSTANCE_ID` env, or the
# `.vast_instance_id` file in the repo root).
#
# Args:
# --run-name <name> (required) — must match the RUN_NAME passed
# to `train_vast.sh`. Checkpoints land under
# training/checkpoints/<run-name>/ locally,
# mirroring ~/training/checkpoints/<run-name>/
# on the Vast box.
# --interval-seconds <n> default 1800 (30 min). Time between rsync
# sweeps. We rely on rsync's own
# incremental transfer so re-pulling an
# unchanged checkpoint is cheap.
# --max-checkpoints <n> default 0 (keep all). When >0, after each
# successful pull we delete oldest
# `checkpoint-<step>` dirs locally until
# only N remain. The `final/` dir is never
# pruned. Disk usage is otherwise the
# operator's responsibility (these dirs are
# git-lfs-ignored).
#
# On SIGTERM/SIGINT we exit cleanly between sweeps (in-flight rsync gets
# the same signal and returns non-zero, which we treat as "try next sweep"
# rather than crash).
#
# Logs to ~/.eliza/checkpoint-sync.log with a 10 MB rotation.
set -euo pipefail
RUN_NAME=""
INTERVAL_SECONDS=1800
MAX_CHECKPOINTS=0
usage() {
cat <<'EOF'
Usage: checkpoint_sync_loop.sh --run-name <name> [--interval-seconds N] [--max-checkpoints N]
Polls a running Vast.ai training instance for new checkpoint-* dirs and
rsyncs them into training/checkpoints/<run-name>/. Run this in a separate
terminal (or under tmux) while train_vast.sh handles the actual training.
Required:
--run-name <name> Must match RUN_NAME passed to train_vast.sh.
Optional:
--interval-seconds <n> Sweep cadence in seconds. Default 1800 (30 min).
--max-checkpoints <n> Keep only the N newest checkpoint-* dirs locally
after each pull (0 = keep all). 'final/' is
never pruned. Default 0.
Env (read, never written):
ELIZA_VAST_INSTANCE_ID Vast instance id. Set by train_vast.sh provision.
VAST_INSTANCE_ID Legacy alias. Honored if ELIZA_* unset.
Outputs:
training/checkpoints/<run-name>/checkpoint-<step>/...
training/checkpoints/<run-name>/_pull-log.jsonl (one line per successful pull)
~/.eliza/checkpoint-sync.log (rotated at 10 MB)
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--run-name)
RUN_NAME="${2:-}"
shift 2
;;
--interval-seconds)
INTERVAL_SECONDS="${2:-}"
shift 2
;;
--max-checkpoints)
MAX_CHECKPOINTS="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "error: unknown arg $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ -z "$RUN_NAME" ]]; then
echo "error: --run-name is required" >&2
usage >&2
exit 2
fi
if ! [[ "$INTERVAL_SECONDS" =~ ^[0-9]+$ ]] || [[ "$INTERVAL_SECONDS" -lt 1 ]]; then
echo "error: --interval-seconds must be a positive integer" >&2
exit 2
fi
if ! [[ "$MAX_CHECKPOINTS" =~ ^[0-9]+$ ]]; then
echo "error: --max-checkpoints must be a non-negative integer" >&2
exit 2
fi
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOCAL_CKPT_DIR="$ROOT/checkpoints/$RUN_NAME"
PULL_LOG="$LOCAL_CKPT_DIR/_pull-log.jsonl"
LOG_FILE="$HOME/.eliza/checkpoint-sync.log"
LOG_MAX_BYTES=$((10 * 1024 * 1024))
mkdir -p "$LOCAL_CKPT_DIR" "$(dirname "$LOG_FILE")"
# ---------------------------------------------------------------------------
# logging — append to LOG_FILE with 10 MB rotation, also echo to stdout.
# ---------------------------------------------------------------------------
log() {
local ts
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
local line="[$ts] [sync $RUN_NAME] $*"
echo "$line"
if [[ -f "$LOG_FILE" ]]; then
local size
size="$(stat -c%s "$LOG_FILE" 2>/dev/null || stat -f%z "$LOG_FILE" 2>/dev/null || echo 0)"
if [[ "$size" -ge "$LOG_MAX_BYTES" ]]; then
mv -f "$LOG_FILE" "$LOG_FILE.1"
fi
fi
echo "$line" >> "$LOG_FILE"
}
# Resolve instance id from env or .vast_instance_id (same precedence as
# train_vast.sh — ELIZA_* preferred, VAST_* as backward-compat alias,
# file as last resort).
resolve_instance_id() {
if [[ -n "${ELIZA_VAST_INSTANCE_ID:-}" ]]; then
echo "$ELIZA_VAST_INSTANCE_ID"
return 0
fi
if [[ -n "${VAST_INSTANCE_ID:-}" ]]; then
echo "$VAST_INSTANCE_ID"
return 0
fi
local id_file="$ROOT/.vast_instance_id"
if [[ -f "$id_file" ]]; then
cat "$id_file"
return 0
fi
return 1
}
# Resolve the SSH endpoint via the existing vast helper. Prints
# "USER HOST PORT" on stdout. Returns non-zero if the helper fails (e.g.
# instance still booting).
ssh_endpoint() {
local instance_id="$1"
( cd "$ROOT" && python3 -m scripts.lib.vast ssh "$instance_id" ) 2>/dev/null
}
# List remote `checkpoint-*` dirs (and `final` if present) under the run.
# Emits one dir name per line (e.g. "checkpoint-200", "final").
list_remote_checkpoints() {
local user="$1" host="$2" port="$3"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o BatchMode=yes -o ConnectTimeout=15 \
-p "$port" "$user@$host" \
"ls -1 ~/training/checkpoints/$RUN_NAME 2>/dev/null | grep -E '^(checkpoint-[0-9]+|final)$' || true"
}
# Local directory size in MB (rounded). Used for the pull-log entry.
dir_size_mb() {
local dir="$1"
du -sm "$dir" 2>/dev/null | awk '{print $1}'
}
# Append one JSONL pull-log entry. Step is the parsed integer step ("final"
# is recorded as -1 here; eval_checkpoint.py promotes it to max+1 at scoring
# time so the progress curve has a final point on the X axis).
append_pull_log() {
local step="$1" dir="$2"
local size_mb
size_mb="$(dir_size_mb "$dir")"
local pulled_at
pulled_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf '{"step": %s, "pulled_at": "%s", "size_mb": %s}\n' \
"$step" "$pulled_at" "${size_mb:-0}" >> "$PULL_LOG"
}
# Prune old checkpoint-<step> dirs when --max-checkpoints is set. Sorts by
# numeric step; never touches `final/`.
prune_old_checkpoints() {
if [[ "$MAX_CHECKPOINTS" -le 0 ]]; then
return 0
fi
local kept=0
# Sort descending by step number, drop anything past MAX_CHECKPOINTS.
while IFS= read -r dir; do
[[ -z "$dir" ]] && continue
kept=$((kept + 1))
if [[ "$kept" -gt "$MAX_CHECKPOINTS" ]]; then
log "prune: removing old checkpoint $dir (kept=$MAX_CHECKPOINTS)"
rm -rf -- "$LOCAL_CKPT_DIR/$dir"
fi
done < <(
ls -1 "$LOCAL_CKPT_DIR" 2>/dev/null \
| grep -E '^checkpoint-[0-9]+$' \
| sort -t- -k2 -n -r
)
}
# Trap SIGTERM/SIGINT so we exit cleanly between sweeps. Inside an active
# rsync the signal will propagate and rsync will exit non-zero — we treat
# that as "this sweep failed, retry next interval" rather than crashing.
SHOULD_EXIT=0
on_signal() {
log "received signal — will exit after current sweep"
SHOULD_EXIT=1
}
trap on_signal TERM INT
log "start: run=$RUN_NAME interval=${INTERVAL_SECONDS}s max_checkpoints=$MAX_CHECKPOINTS local=$LOCAL_CKPT_DIR"
while true; do
if [[ "$SHOULD_EXIT" -eq 1 ]]; then
log "exit: signal received"
exit 0
fi
instance_id=""
if instance_id="$(resolve_instance_id)" && [[ -n "$instance_id" ]]; then
log "sweep: instance=$instance_id"
if endpoint="$(ssh_endpoint "$instance_id")" && [[ -n "$endpoint" ]]; then
read -r user host port <<< "$endpoint"
remote_list="$(list_remote_checkpoints "$user" "$host" "$port" || true)"
if [[ -z "$remote_list" ]]; then
log "sweep: no remote checkpoints yet"
else
# Compare against locally-present checkpoint dirs to skip the
# "no new step" case (rsync would still run, but the log entry
# would be misleading).
while IFS= read -r remote_dir; do
[[ -z "$remote_dir" ]] && continue
step="-1"
if [[ "$remote_dir" =~ ^checkpoint-([0-9]+)$ ]]; then
step="${BASH_REMATCH[1]}"
fi
local_dir="$LOCAL_CKPT_DIR/$remote_dir"
had_local=0
[[ -d "$local_dir" ]] && had_local=1
log "pull: $remote_dir (step=$step had_local=$had_local)"
# rsync the dir contents. We accept partial transfers (network
# blips) and don't bail the loop if a single dir fails.
if rsync -avh --partial --info=stats1 \
-e "ssh -p $port -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=30" \
"$user@$host:training/checkpoints/$RUN_NAME/$remote_dir/" \
"$local_dir/" >> "$LOG_FILE" 2>&1; then
# Only append a pull-log entry when the dir is genuinely new
# locally OR its content changed (mtime newer than the log
# entry). Cheapest correct check: if it's new, log; otherwise
# only log when rsync actually transferred bytes. To keep the
# log simple and the JSONL append-only we always log on a
# successful pull but mark whether the dir was new.
if [[ "$had_local" -eq 0 ]]; then
append_pull_log "$step" "$local_dir"
log "pull: $remote_dir done (new, step=$step)"
else
log "pull: $remote_dir done (refresh, step=$step) — no log entry"
fi
else
log "pull: $remote_dir FAILED (rsync exit non-zero, will retry next sweep)"
fi
done <<< "$remote_list"
prune_old_checkpoints
fi
else
log "sweep: ssh endpoint not resolvable yet (instance still booting?)"
fi
else
log "sweep: no ELIZA_VAST_INSTANCE_ID / VAST_INSTANCE_ID / .vast_instance_id — skipping"
fi
if [[ "$SHOULD_EXIT" -eq 1 ]]; then
log "exit: signal received"
exit 0
fi
# Sleep in 1-second chunks so SIGTERM lands fast.
remaining="$INTERVAL_SECONDS"
while [[ "$remaining" -gt 0 ]]; do
sleep 1
remaining=$((remaining - 1))
if [[ "$SHOULD_EXIT" -eq 1 ]]; then
log "exit: signal received during sleep"
exit 0
fi
done
done