Spaces:
Paused
Paused
File size: 6,402 Bytes
0dd6c2f | 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 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | """
Upload a tau-bench experiment directory (or just its archived best checkpoints) to the Hugging Face Hub.
Default scope is the entire experiment directory:
`.art/<project>/models/<experiment>/`
The legacy "best_models" scope uploads only archived checkpoints under:
`.art/<project>/models/<experiment>/best_models/<split>/<step>/`
Usage (dry-run first):
uv run python linalg_zero/grpo/tau-bench/push_best_models_to_hf.py \
--project linalgzero-grpo \
--experiment linalgzero-grpo-001 \
--hub-namespace atomwalk12 \
--dry-run
Auth:
- Run `huggingface-cli login` beforehand, or set `HF_TOKEN` in your environment.
"""
from __future__ import annotations
import argparse
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
@dataclass(frozen=True)
class BestModelCheckpoint:
model: str
split: str
step: int
path: Path
_HF_REPO_NAME_ALLOWED = re.compile(r"[^A-Za-z0-9_.-]+")
_DEFAULT_IGNORE_PATTERNS: tuple[str, ...] = (
"**/__pycache__/**",
"**/.DS_Store",
)
def _sanitize_hf_repo_name(name: str) -> str:
# Hugging Face repo names allow: letters, numbers, "-", "_", "."
# Convert runs of other chars to a single "-".
name = _HF_REPO_NAME_ALLOWED.sub("-", name).strip("-.")
name = re.sub(r"-{2,}", "-", name)
if not name:
raise ValueError("Sanitized repo name is empty; choose a different template.")
return name
def _discover_best_model_checkpoints(best_models_dir: Path) -> list[BestModelCheckpoint]:
checkpoints: list[BestModelCheckpoint] = []
if not best_models_dir.is_dir():
return checkpoints
for split_dir in sorted(p for p in best_models_dir.iterdir() if p.is_dir()):
split = split_dir.name
for step_dir in sorted(p for p in split_dir.iterdir() if p.is_dir()):
if not step_dir.name.isdigit():
continue
checkpoints.append(
BestModelCheckpoint(
model=best_models_dir.parent.name,
split=split,
step=int(step_dir.name),
path=step_dir,
)
)
return checkpoints
def _format_repo_id(
*, namespace: str, project: str, experiment: str, scope: Literal["experiment", "best_models"]
) -> str:
suffix = "experiment" if scope == "experiment" else "best-models"
repo_name = _sanitize_hf_repo_name(f"{project}--{experiment}--{suffix}")
return f"{namespace}/{repo_name}"
def main() -> int:
parser = argparse.ArgumentParser(description="Push one experiment directory to HF Hub.")
parser.add_argument("--project", required=True, help="Project directory under `.art/` (e.g. linalgzero-grpo).")
parser.add_argument(
"--experiment",
required=True,
help="Experiment/model directory name under `.art/<project>/models/` (e.g. linalgzero-grpo-001).",
)
parser.add_argument("--hub-namespace", required=True, help="HF namespace/user/org (e.g. atomwalk12).")
parser.add_argument(
"--scope",
choices=["experiment", "best_models"],
default="experiment",
help="What to upload: the whole experiment directory (default) or only `best_models/` checkpoints.",
)
parser.add_argument("--dry-run", action="store_true", help="List planned uploads without pushing.")
args = parser.parse_args()
experiment_dir = Path(".art") / args.project / "models" / args.experiment
if not experiment_dir.is_dir():
print(f"Not found: {experiment_dir}")
print("Expected layout: .art/<project>/models/<experiment>/")
return 1
scope: Literal["experiment", "best_models"] = args.scope
repo_id = _format_repo_id(
namespace=args.hub_namespace,
project=args.project,
experiment=args.experiment,
scope=scope,
)
print(f"Repo: https://huggingface.co/{repo_id}")
if scope == "experiment":
print("Planned upload:")
print(f"- {experiment_dir} -> (repo root)")
else:
best_models_dir = experiment_dir / "best_models"
if not best_models_dir.is_dir():
print(f"Not found: {best_models_dir}")
print("Expected layout: .art/<project>/models/<experiment>/best_models/<split>/<step>/")
return 1
checkpoints = _discover_best_model_checkpoints(best_models_dir)
if not checkpoints:
print(f"No checkpoints found under {best_models_dir}/*/*")
return 1
planned: list[tuple[BestModelCheckpoint, str]] = [
(c, f"best_models/{c.split}/{c.step:04d}") for c in checkpoints
]
print("Planned uploads:")
for ckpt, path_in_repo in planned:
print(f"- {ckpt.path} -> {path_in_repo}/")
if args.dry_run:
return 0
try:
from huggingface_hub import HfApi
except Exception as e: # pragma: no cover
print("Missing dependency: huggingface_hub. Install it in your environment to use this script.")
print(f"Import error: {e}")
return 1
api = HfApi()
api.create_repo(repo_id=repo_id, private=False, exist_ok=True)
if scope == "experiment":
print(f"\nUploading {experiment_dir} -> {repo_id}:(repo root)")
api.upload_folder(
repo_id=repo_id,
repo_type="model",
folder_path=str(experiment_dir),
path_in_repo="",
commit_message=f"Upload {args.project}/{args.experiment} experiment directory",
ignore_patterns=list(_DEFAULT_IGNORE_PATTERNS),
)
else:
best_models_dir = experiment_dir / "best_models"
checkpoints = _discover_best_model_checkpoints(best_models_dir)
planned = [(c, f"best_models/{c.split}/{c.step:04d}") for c in checkpoints]
for ckpt, path_in_repo in planned:
print(f"\nUploading {ckpt.path} -> {repo_id}:{path_in_repo}/")
api.upload_folder(
repo_id=repo_id,
repo_type="model",
folder_path=str(ckpt.path),
path_in_repo=path_in_repo,
commit_message=f"Upload {args.project}/{args.experiment}:{path_in_repo}/",
ignore_patterns=list(_DEFAULT_IGNORE_PATTERNS),
)
print("\nDone.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
|