File size: 5,820 Bytes
c8c36ec | 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 | #!/usr/bin/env python3
"""Stage this folder with ../core into a Docker Space layout, and optionally publish it.
python deploy.py --stage-only --stage-dir /tmp/retroenv-space # build locally
docker build -t retroenv /tmp/retroenv-space
python deploy.py --repo FineEnvs/retroenv --public # the FineEnvs Space
The Space runs the same code as a local checkout and serves the same pinned snapshot: the
image carries code and corpus-manifest.json only. The serving bucket named in the manifest
is mounted read-only at /data, and prepare.py copies and checks the snapshot from there at
startup. Every file the manifest names must already be in the bucket, so a Space never
starts against a half-published snapshot.
"""
from __future__ import annotations
import argparse
import json
import shutil
import tempfile
from pathlib import Path
HERE = Path(__file__).resolve().parent
ENV_ROOT = HERE.parent
MANIFEST = HERE / "corpus-manifest.json"
MOUNT = "/data"
IGNORE = shutil.ignore_patterns("__pycache__", "*.egg-info", ".pytest_cache", ".venv", ".env", "prepared", "tests")
def stage(target: Path) -> Path:
if target.exists():
shutil.rmtree(target)
target.mkdir(parents=True)
shutil.copytree(ENV_ROOT / "core", target / "core", ignore=IGNORE)
shutil.copytree(HERE, target / "openenv", ignore=IGNORE)
shutil.copy2(HERE / "Dockerfile", target / "Dockerfile")
shutil.copy2(HERE / "README.md", target / "README.md")
shutil.copy2(ENV_ROOT / ".dockerignore", target / ".dockerignore")
return target
def live_bucket(api, bucket_id: str) -> str:
"""The bucket's current name. An organization rename keeps HTTP reads working through a
redirect but breaks a Space's volume mount, so the mount uses the live name."""
return api.bucket_info(bucket_id).id
def check_bucket(api, bucket: str, manifest: dict) -> None:
entries = [*manifest["files"], *manifest.get("evalsets", {}).values()]
expected = {entry["path"]: entry["size"] for entry in entries}
found = {f.path: f.size for f in api.get_bucket_paths_info(bucket, list(expected))}
missing = sorted(path for path, size in expected.items() if found.get(path) != size)
if missing:
raise SystemExit(f"{bucket} lacks or has wrong sizes for {missing[:10]}; publish the snapshot first")
print(f"verified {len(expected)} snapshot files in {bucket}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--repo", help="Space id, e.g. FineEnvs/retroenv")
parser.add_argument("--public", action="store_true", help="create the Space public (default private)")
parser.add_argument("--concurrency", type=int, default=64, help="WebSocket sessions per process")
parser.add_argument("--toolset", choices=("full", "unaided"), default="full")
parser.add_argument("--hardware", help="Space hardware flavor, e.g. cpu-upgrade (default: leave as is)")
parser.add_argument("--stage-dir", type=Path)
parser.add_argument("--stage-only", action="store_true")
args = parser.parse_args()
manifest = json.loads(MANIFEST.read_text())
if manifest.get("status") != "ready":
raise SystemExit(f"{MANIFEST} is not a finished snapshot; run dataset/publish_bucket.py")
staged = stage(args.stage_dir or Path(tempfile.mkdtemp(prefix="retroenv-space-")))
print(f"staged {staged}")
if args.stage_only:
return 0
if not args.repo:
parser.error("--repo is required to publish")
from huggingface_hub import HfApi, Volume
api = HfApi()
bucket = live_bucket(api, manifest["bucket_id"])
check_bucket(api, bucket, manifest)
api.create_repo(args.repo, repo_type="space", space_sdk="docker", private=not args.public, exist_ok=True)
commit = api.upload_folder(
repo_id=args.repo,
repo_type="space",
folder_path=staged,
commit_message=f"Serve RetroEnv snapshot {manifest['snapshot_id'][:12]}",
)
runtime = api.space_info(args.repo).runtime
current = list(runtime.volumes or []) if runtime else []
volumes = [v for v in current if v.mount_path != MOUNT]
volumes.append(Volume(type="bucket", source=bucket, mount_path=MOUNT, read_only=True))
if [v.to_dict() for v in volumes] != [v.to_dict() for v in current]:
api.set_space_volumes(args.repo, volumes=volumes)
settings = {
"MAX_CONCURRENT_ENVS": str(args.concurrency),
"ENABLE_WEB_INTERFACE": "true",
"RETROENV_TOOLSET": args.toolset,
"RETROENV_BUCKET_ROOT": MOUNT,
}
existing = api.get_space_variables(args.repo)
for key, value in settings.items():
if key not in existing or existing[key].value != value:
api.add_space_variable(args.repo, key, value)
# Either of these would take precedence over the snapshot and serve something else.
for stale in ("RETROENV_TASKS_REPO", "RETROENV_BENCHMARK_DIR"):
if stale in existing:
api.delete_space_variable(args.repo, stale)
print(f"removed the stale {stale} variable")
if args.hardware:
api.request_space_hardware(args.repo, args.hardware)
print(
json.dumps(
{
"space": f"https://huggingface.co/spaces/{args.repo}",
"commit": commit.oid,
"snapshot_id": manifest["snapshot_id"],
"bucket": bucket,
"splits": {name: info["tasks"] for name, info in manifest["splits"].items()}
| {name: info["tasks"] for name, info in manifest["evalsets"].items()},
"sessions": args.concurrency,
},
indent=2,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
|