#!/usr/bin/env python3 from __future__ import annotations import argparse import hashlib import json import os import shutil import subprocess import sys import time from datetime import datetime, timezone from pathlib import Path from typing import Any from huggingface_hub import HfApi def iso_now() -> str: return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") def stamp_now() -> str: return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") def load_json(path: Path, default: Any) -> Any: try: return json.loads(path.read_text(encoding="utf-8")) except Exception: return default def save_json(path: Path, data: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, indent=2, sort_keys=True), encoding="utf-8") def sha256_file(path: Path) -> str: h = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1 << 22), b""): h.update(chunk) return h.hexdigest() def upload_file(api: HfApi, repo_id: str, local_path: Path, remote_path: str, message: str) -> None: api.upload_file( repo_id=repo_id, path_or_fileobj=str(local_path), path_in_repo=remote_path, commit_message=message, ) def delete_remote_not_kept(api: HfApi, repo_id: str, remote_dir: str, keep_basenames: set[str]) -> list[str]: deleted: list[str] = [] try: files = api.list_repo_files(repo_id=repo_id) except Exception as exc: print(f"[upload] WARN list_repo_files failed: {exc}", flush=True) return deleted prefix = remote_dir.rstrip("/") + "/" victims = [] for file_path in files: if not file_path.startswith(prefix): continue name = Path(file_path).name base = name[:-7] if name.endswith(".sha256") else name if base not in keep_basenames: victims.append(file_path) if victims: try: api.delete_files(repo_id=repo_id, paths=victims, commit_message=f"Prune AGILLM4 uploads under {remote_dir}") deleted.extend(victims) except Exception as exc: print(f"[upload] WARN delete_files failed for {len(victims)} files: {exc}", flush=True) return deleted def latest_file(glob_root: Path, pattern: str) -> Path | None: files = [p for p in glob_root.glob(pattern) if p.is_file()] return max(files, key=lambda p: p.stat().st_mtime) if files else None def latest_file_excluding(glob_root: Path, pattern: str, excluded_name_parts: tuple[str, ...]) -> Path | None: files = [ p for p in glob_root.glob(pattern) if p.is_file() and not any(part in p.name for part in excluded_name_parts) ] return max(files, key=lambda p: p.stat().st_mtime) if files else None def checkpoint_artifacts(path: Path) -> list[Path]: return [ path, path.with_suffix(".sha256"), path.with_suffix(path.suffix + ".upload.sha256"), path.with_suffix(path.suffix + ".dtmp"), ] def prune_local_checkpoints( save_dir: Path, pattern: str, keep: int, label: str, excluded_name_parts: tuple[str, ...] = (), ) -> list[str]: if keep < 0: return [] files = sorted( [ p for p in save_dir.glob(pattern) if p.is_file() and p.stat().st_size > 0 and not any(part in p.name for part in excluded_name_parts) ], key=lambda p: p.stat().st_mtime, ) victims = files[:max(0, len(files) - keep)] deleted: list[str] = [] for path in victims: for artifact in checkpoint_artifacts(path): try: if artifact.exists(): artifact.unlink() deleted.append(str(artifact)) except Exception as exc: print(f"[upload] WARN local {label} prune failed for {artifact}: {exc}", flush=True) if deleted: print(f"[upload] pruned {len(deleted)} local {label} artifacts", flush=True) return deleted def status_json(script: Path, log: Path, save_dir: Path) -> dict[str, Any]: result = subprocess.run( [sys.executable, "-u", str(script), "status", "--json", "--log", str(log), "--save_dir", str(save_dir)], capture_output=True, text=True, timeout=60, check=False, ) if result.returncode != 0: return {"checked_at": iso_now(), "error": result.stderr.strip() or result.stdout.strip()} try: return json.loads(result.stdout) except Exception: return {"checked_at": iso_now(), "error": "failed to parse status json", "raw": result.stdout[-4000:]} def write_tail(src: Path, dst: Path, lines: int) -> None: dst.parent.mkdir(parents=True, exist_ok=True) if not src.exists(): dst.write_text("", encoding="utf-8") return result = subprocess.run(["tail", "-n", str(lines), str(src)], capture_output=True, text=True, check=False) dst.write_text(result.stdout, encoding="utf-8", errors="replace") def maybe_upload_large( api: HfApi, repo_id: str, state: dict[str, Any], kind: str, path: Path | None, remote_dir: str, interval_sec: int, keep: int, ) -> bool: if path is None or not path.exists(): print(f"[upload] no {kind} checkpoint yet", flush=True) return False now = time.time() last_t = float(state.get(f"last_{kind}_upload_time", 0) or 0) identity = f"{path.name}:{path.stat().st_size}:{int(path.stat().st_mtime)}" if state.get(f"last_{kind}_identity") == identity: print(f"[upload] {kind} unchanged: {path.name}", flush=True) return False if last_t and now - last_t < interval_sec: remaining = int(interval_sec - (now - last_t)) print(f"[upload] {kind} interval not due for {remaining}s: {path.name}", flush=True) return False digest = sha256_file(path) sha_path = path.with_suffix(path.suffix + ".upload.sha256") sha_path.write_text(f"{digest} {path.name}\n", encoding="utf-8") remote_name = f"{stamp_now()}_{path.name}" remote_path = f"{remote_dir.rstrip('/')}/{remote_name}" print(f"[upload] uploading {kind}: {path} -> {repo_id}/{remote_path}", flush=True) upload_file(api, repo_id, path, remote_path, f"Upload AGILLM4 {kind} checkpoint {path.name}") upload_file(api, repo_id, sha_path, remote_path + ".sha256", f"Upload AGILLM4 {kind} checksum {path.name}") history = list(state.get(f"{kind}_uploads", [])) history.append({"name": remote_name, "remote_path": remote_path, "sha256": digest, "uploaded_at": iso_now(), "size": path.stat().st_size}) history = history[-max(1, keep):] state[f"{kind}_uploads"] = history state[f"last_{kind}_upload_time"] = now state[f"last_{kind}_identity"] = identity keep_names = {item["name"] for item in history} deleted = delete_remote_not_kept(api, repo_id, remote_dir, keep_names) if deleted: print(f"[upload] pruned {len(deleted)} remote {kind} files", flush=True) return True def main() -> int: parser = argparse.ArgumentParser(description="Bounded AGILLM4 checkpoint uploader") parser.add_argument("--repo", default=os.environ.get("AGILLM4_UPLOAD_REPO", "OpenTransformer/AGILLM-4")) parser.add_argument("--prefix", default=os.environ.get("AGILLM4_UPLOAD_PREFIX", "training/agillm4_floor_v47_v4pro")) parser.add_argument("--save-dir", type=Path, default=Path(os.environ.get("AGILLM4_UPLOAD_SAVE_DIR", "/workspace/agillm4_4090_ckpts"))) parser.add_argument("--log", type=Path, default=Path(os.environ.get("AGILLM4_UPLOAD_LOG", "/workspace/agillm4_floor_train.log"))) parser.add_argument("--script", type=Path, default=Path(os.environ.get("AGILLM4_UPLOAD_SCRIPT", "/workspace/agillm-4/nB300_agillm4.py"))) parser.add_argument("--state", type=Path, default=Path(os.environ.get("AGILLM4_UPLOAD_STATE", "/workspace/agillm4_upload_state_v4pro.json"))) parser.add_argument("--stage", type=Path, default=Path(os.environ.get("AGILLM4_UPLOAD_STAGE", "/workspace/agillm4_upload_stage_v4pro"))) parser.add_argument("--full-interval-sec", type=int, default=int(os.environ.get("AGILLM4_UPLOAD_FULL_INTERVAL_SEC", str(7 * 24 * 3600)))) parser.add_argument("--delta-interval-sec", type=int, default=int(os.environ.get("AGILLM4_UPLOAD_DELTA_INTERVAL_SEC", str(24 * 3600)))) parser.add_argument("--keep-full", type=int, default=int(os.environ.get("AGILLM4_UPLOAD_KEEP_FULL", "2"))) parser.add_argument("--keep-delta", type=int, default=int(os.environ.get("AGILLM4_UPLOAD_KEEP_DELTA", "2"))) parser.add_argument("--local-keep-full", type=int, default=int(os.environ.get("AGILLM4_LOCAL_KEEP_FULL", "1"))) parser.add_argument("--local-keep-delta", type=int, default=int(os.environ.get("AGILLM4_LOCAL_KEEP_DELTA", "1"))) parser.add_argument("--tail-lines", type=int, default=int(os.environ.get("AGILLM4_UPLOAD_TAIL_LINES", "5000"))) args = parser.parse_args() api = HfApi() prefix = args.prefix.strip("/") args.stage.mkdir(parents=True, exist_ok=True) state = load_json(args.state, {}) local_pruned = { "delta": prune_local_checkpoints(args.save_dir, "*_delta_step*.pt", args.local_keep_delta, "delta"), "full": prune_local_checkpoints(args.save_dir, "*_step*.pt", args.local_keep_full, "full", ("_delta_step",)), } status = status_json(args.script, args.log, args.save_dir) status["upload_policy"] = { "full_interval_sec": args.full_interval_sec, "delta_interval_sec": args.delta_interval_sec, "keep_full_current_files": args.keep_full, "keep_delta_current_files": args.keep_delta, "local_keep_full_files": args.local_keep_full, "local_keep_delta_files": args.local_keep_delta, "note": "Small status/log tail uploads are frequent; multi-GB deltas/full checkpoints are rate-limited for HF public storage.", } status["local_pruned"] = local_pruned status_path = args.stage / "status.json" save_json(status_path, status) upload_file(api, args.repo, status_path, f"{prefix}/status/status.json", "Update AGILLM4 training status") tail_path = args.stage / "train_tail.log" write_tail(args.log, tail_path, args.tail_lines) upload_file(api, args.repo, tail_path, f"{prefix}/logs/train_tail.log", "Update AGILLM4 training log tail") latest_json = args.save_dir / "latest.json" if latest_json.exists(): shutil.copy2(latest_json, args.stage / "latest.json") upload_file(api, args.repo, args.stage / "latest.json", f"{prefix}/status/latest.json", "Update AGILLM4 latest checkpoint metadata") newest_delta = latest_file(args.save_dir, "*_delta_step*.pt") newest_full = latest_file_excluding(args.save_dir, "*_step*.pt", ("_delta_step",)) maybe_upload_large(api, args.repo, state, "delta", newest_delta, f"{prefix}/checkpoints/deltas", args.delta_interval_sec, args.keep_delta) maybe_upload_large(api, args.repo, state, "full", newest_full, f"{prefix}/checkpoints/full", args.full_interval_sec, args.keep_full) state["last_status_upload_at"] = iso_now() save_json(args.state, state) manifest_path = args.stage / "upload_state.json" save_json(manifest_path, state) upload_file(api, args.repo, manifest_path, f"{prefix}/status/upload_state.json", "Update AGILLM4 upload state") print(f"[upload] done {iso_now()} repo={args.repo} prefix={prefix}", flush=True) return 0 if __name__ == "__main__": raise SystemExit(main())