| |
| """Upload scratch/gandalf staging files to Hugging Face. |
| |
| Runs on the login node (python3 + huggingface_hub). The feanor sidecar only |
| needs to land files in --stage-dir; this process does the HTTP/LFS upload. |
| """ |
| import argparse |
| import json |
| import os |
| import sys |
| import time |
| import traceback |
|
|
|
|
| def _load_json(path): |
| try: |
| with open(path) as f: |
| return json.load(f) |
| except Exception: |
| return {} |
|
|
|
|
| def _write_json(path, payload): |
| tmp = path + ".tmp" |
| with open(tmp, "w") as f: |
| json.dump(payload, f, indent=2, sort_keys=True) |
| f.write("\n") |
| os.replace(tmp, path) |
|
|
|
|
| _KEEP_HB = ( |
| "event", "step", "stage", "loss", |
| "val_acc", "val_mass", "val_excess", "val_spread", |
| "val_mass_multi", "val_excess_multi", "val_out_multi", "val_kl_multi", |
| "val_hbound", "val_hgap", "val_hbound_puz", |
| "val_ceq", "val_hq", "val_ceq_ok", "val_ceq_gap", |
| "loc_wave", "loc_coverage", "loc_lcs", "loc_dup", |
| "puzzle_acc", "last_ckpt_step", |
| ) |
|
|
|
|
| def _slim_heartbeat(src, dest): |
| raw = _load_json(src) |
| slim = {k: raw[k] for k in _KEEP_HB if k in raw} |
| _write_json(dest, slim) |
| return dest |
|
|
|
|
| def _slim_log(src, dest): |
| """Keep only eval / promote lines: step, stage, loss, accuracies.""" |
| keep = [] |
| try: |
| with open(src, errors="replace") as f: |
| for line in f: |
| s = line.strip() |
| if not s: |
| continue |
| if s.startswith("[curriculum]"): |
| keep.append(s + "\n") |
| continue |
| |
| if " loss " in s and "stage" in s and not s.startswith("I"): |
| keep.append(s + "\n") |
| except OSError: |
| return None |
| with open(dest, "w") as f: |
| f.writelines(keep) |
| return dest |
|
|
|
|
| def _upload(api, token, local, repo, dest): |
| api.upload_file( |
| path_or_fileobj=local, |
| path_in_repo=dest, |
| repo_id=repo, |
| repo_type="model", |
| token=token, |
| commit_message=f"sync {dest}", |
| ) |
| print(f"[hf_push] uploaded {dest} ({os.path.getsize(local)} bytes)", flush=True) |
|
|
|
|
| def sync_once(args, state, api, token): |
| prefix = args.prefix.strip("/") |
| repo = args.repo |
|
|
| for local, dest, key in ( |
| (args.heartbeat, f"{prefix}/heartbeat.json", "heartbeat"), |
| (args.log, f"{prefix}/train.log", "log"), |
| ): |
| if not local or not os.path.isfile(local): |
| continue |
| mtime = os.path.getmtime(local) |
| size = os.path.getsize(local) |
| last = state.get(key, {}) |
| min_age = 0 if key == "heartbeat" else 300 |
| due = (time.time() - float(last.get("uploaded_at", 0))) >= min_age |
| if due and (mtime > float(last.get("mtime", 0)) or size != last.get("size", -1)): |
| upload_path = local |
| if key == "heartbeat": |
| upload_path = _slim_heartbeat( |
| local, os.path.join(os.path.dirname(local) or ".", |
| "_hf_heartbeat_slim.json")) |
| elif key == "log": |
| upload_path = _slim_log( |
| local, os.path.join(os.path.dirname(local) or ".", |
| "_hf_train_slim.log")) |
| if upload_path: |
| _upload(api, token, upload_path, repo, dest) |
| state[key] = {"mtime": mtime, "size": size, "uploaded_at": time.time()} |
|
|
| if args.stage_dir and os.path.isdir(args.stage_dir): |
| for name in sorted(os.listdir(args.stage_dir)): |
| if name in ("hf_push_state.json", "heartbeat.json"): |
| continue |
| if not name.endswith(".tar") and name not in ("latest.json",): |
| continue |
| local = os.path.join(args.stage_dir, name) |
| if not os.path.isfile(local): |
| continue |
| |
| |
| |
| |
| |
| rolling = name in ("latest.tar", "latest.json") |
| mtime = os.path.getmtime(local) |
| if rolling: |
| if mtime <= float(state.get("rolling", {}).get(name, 0)): |
| continue |
| elif name in state.get("staged", []): |
| continue |
| dest = f"{prefix}/{name}" |
| if name.startswith("stage"): |
| dest = f"{prefix}/stages/{name}" |
| elif name.startswith("step_"): |
| dest = f"{prefix}/steps/{name}" |
| _upload(api, token, local, repo, dest) |
| if rolling: |
| state.setdefault("rolling", {})[name] = mtime |
| continue |
| state.setdefault("staged", []).append(name) |
| |
| try: |
| os.remove(local) |
| except OSError: |
| pass |
| return state |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--repo", default="Avra98/Sudoku_superposition") |
| ap.add_argument("--prefix", default="runs/w12_inst_latent") |
| ap.add_argument("--heartbeat", default="") |
| ap.add_argument("--log", default="") |
| ap.add_argument("--stage-dir", default="") |
| ap.add_argument("--token-file", default="/scratch/users/gatmiry/.hf_token") |
| ap.add_argument("--state", default="") |
| ap.add_argument("--interval", type=int, default=60) |
| ap.add_argument("--once", action="store_true") |
| args = ap.parse_args() |
|
|
| token = "" |
| if args.token_file and os.path.isfile(args.token_file): |
| with open(args.token_file) as f: |
| token = f.read().strip() |
| token = token or os.environ.get("HF_TOKEN", "") |
| if not token: |
| print("[hf_push] no token", file=sys.stderr) |
| return 1 |
|
|
| from huggingface_hub import HfApi |
| api = HfApi(token=token) |
| state_path = args.state or os.path.join( |
| args.stage_dir or "/tmp/sudoku_hf_uploads", "hf_push_state.json") |
| os.makedirs(os.path.dirname(state_path) or ".", exist_ok=True) |
| state = _load_json(state_path) |
| print(f"[hf_push] watching heartbeat={args.heartbeat} log={args.log} " |
| f"stage={args.stage_dir}", flush=True) |
| while True: |
| try: |
| state = sync_once(args, state, api, token) |
| _write_json(state_path, state) |
| except Exception: |
| traceback.print_exc() |
| print("[hf_push] cycle failed; will retry", flush=True) |
| if args.once: |
| break |
| time.sleep(max(5, args.interval)) |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|