Spaces:
Sleeping
Sleeping
File size: 11,754 Bytes
ed7ea89 84cbfcb ed7ea89 4f929dc ed7ea89 4f929dc ed7ea89 f2448fa ed7ea89 4f929dc ed7ea89 4f929dc ed7ea89 f2448fa ed7ea89 ebbdbbf ed7ea89 ebbdbbf ed7ea89 84cbfcb ed7ea89 4f929dc 84cbfcb ed7ea89 4f929dc ed7ea89 4f929dc ed7ea89 | 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 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | #!/usr/bin/env python3
"""Measure real, authenticated Space outputs. Never imports the app locally."""
import argparse
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import random
import shutil
import sys
import time
from gradio_client import Client, handle_file
from huggingface_hub import HfApi, get_token
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from settings import DEFAULT_QUALITY, EXAMPLE_PROMPTS, SIZES, STEPS, duration_cap
# Held out from the four UI studies; do not change these after seeing outputs.
CHALLENGE_PROMPTS = [
"A straight-on studio photograph of exactly three objects on one white shelf: "
"a red wooden cube on the left, a green ceramic sphere in the middle, and a blue "
"metal pyramid on the right. All objects are separate, equally spaced and fully "
"visible. Soft neutral lighting, no other objects, no text.",
"A documentary portrait of an adult female ceramic artist at a wooden workbench. "
"Both of her hands are fully visible, one on either side of a single small white "
"ceramic bowl. Five natural fingers on each hand. Linen shirt, daylight from a "
"large window, realistic skin and clay texture, calm expression, eye-level camera.",
]
def sha256(path):
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def file_path(value):
if isinstance(value, str):
return value
if isinstance(value, dict):
return value.get("path") or value.get("name")
raise ValueError(f"Unexpected file response type: {type(value).__name__}")
def trials(suite, repeats):
if suite == "smoke":
return [{"prompt_index": 0, "quality": "Draft · 20 steps", "resolution": "1K",
"aspect": "1:1", "seed": 42, "repeat": 0}]
if suite == "repeatability":
return [{"prompt_index": 0, "quality": DEFAULT_QUALITY, "resolution": "1K",
"aspect": "1:1", "seed": 42, "repeat": r} for r in range(repeats)]
if suite == "shapes":
return [{"prompt_index": 0, "quality": "Draft · 20 steps", "resolution": "1K",
"aspect": aspect, "seed": 42, "repeat": 0} for aspect in SIZES["1K"]]
if suite == "resolution":
return [{"prompt_index": 0, "quality": DEFAULT_QUALITY, "resolution": resolution,
"aspect": "1:1", "seed": 42, "repeat": r}
for r in range(repeats) for resolution in ["1K", "2K"]]
if suite == "steps":
rows = [{"prompt_index": p, "quality": quality, "resolution": "1K",
"aspect": "1:1", "seed": seed, "repeat": r}
for r in range(repeats) for p in range(4) for seed in [42, 12345]
for quality in STEPS]
random.Random(20260920).shuffle(rows)
return rows
if suite == "challenge":
rows = [{"prompt_index": p+4, "prompt": prompt, "quality": quality, "resolution": "1K",
"aspect": "1:1", "seed": seed, "repeat": r}
for r in range(repeats) for p, prompt in enumerate(CHALLENGE_PROMPTS)
for seed in [42, 12345] for quality in STEPS]
random.Random(20260921).shuffle(rows)
return rows
raise ValueError(suite)
def main():
sys.stdout.reconfigure(line_buffering=True)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("space")
parser.add_argument("--suite", choices=["smoke", "repeatability", "shapes", "resolution", "steps", "challenge"], default="smoke")
parser.add_argument("--repeats", type=int, default=3)
parser.add_argument("--max-calls", type=int, default=1,
help="Explicit call budget. Increase only after checking remaining ZeroGPU quota.")
parser.add_argument("--max-gpu-seconds", type=float, default=180,
help="Stop before another call if observed usage plus the last requested cap exceeds this budget.")
parser.add_argument("--cold-after-restart", action="store_true",
help="Assert a separately observed restart; only first app request can qualify.")
parser.add_argument("--reference", type=Path)
parser.add_argument("--prompt", help="Override the curated prompt, useful for edit verification.")
parser.add_argument("--transparent", action="store_true")
parser.add_argument("--resolution", choices=list(SIZES))
parser.add_argument("--quality", choices=list(STEPS))
parser.add_argument("--aspect", choices=list(SIZES["1K"]))
parser.add_argument("--seed", type=int)
args = parser.parse_args()
if args.repeats < 1 or args.max_calls < 1 or args.max_gpu_seconds <= 0:
parser.error("Repeats, call count, and GPU budget must be positive.")
token = get_token()
if not token:
sys.exit("BLOCKED: Existing Hugging Face authentication is required; no anonymous private-Space calls.")
api = HfApi(token=token)
info = api.space_info(args.space)
runtime = api.get_space_runtime(args.space)
if str(runtime.stage) != "RUNNING" or str(runtime.hardware) != "zero-a10g":
sys.exit(f"Space must be RUNNING on zero-a10g: stage={runtime.stage}, hardware={runtime.hardware}")
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")
out = ROOT / "artifacts" / "benchmarks" / f"{stamp}-{args.suite}"
out.mkdir(parents=True, exist_ok=False)
client = Client(args.space, token=token, httpx_kwargs={"timeout": 600},
download_files=str(out / "downloads"), verbose=False, analytics_enabled=False)
discovered = client.view_api(return_format="dict")
(out / "api.json").write_text(json.dumps(discovered, indent=2, default=str))
named = discovered.get("named_endpoints", {})
if "/generate" not in named:
sys.exit("/generate is absent from discovered API. Inspect api.json before making a request.")
variables = api.get_space_variables(args.space)
manifest = {"space": args.space, "hub_commit": info.sha, "private": info.private,
"runtime": runtime.raw, "suite": args.suite, "started_at": stamp,
"cold_assertion": args.cold_after_restart,
"max_calls": args.max_calls, "gpu_budget_seconds": args.max_gpu_seconds,
"reference_sha256": sha256(args.reference) if args.reference else None,
"experimental_variables": {key: variables[key].value for key in (
"STILL_GPU_SIZE", "STILL_GPU_SECONDS", "STILL_KV_CACHE", "STILL_VAE_TILING")
if key in variables}}
(out / "manifest.json").write_text(json.dumps(manifest, indent=2, default=str))
configured_cap = int(variables["STILL_GPU_SECONDS"].value) if "STILL_GPU_SECONDS" in variables else None
configured_size = variables["STILL_GPU_SIZE"].value if "STILL_GPU_SIZE" in variables else "xlarge"
spent = 0.0
records = []
for index, trial in enumerate(trials(args.suite, args.repeats)[:args.max_calls]):
trial.update({key: getattr(args, key) for key in ["resolution", "quality", "aspect", "seed"]
if getattr(args, key) is not None})
cap = configured_cap * (1 if configured_size == "xlarge" else 1.5) if configured_cap else duration_cap(
STEPS[trial["quality"]], int(trial["resolution"][0])*1024, bool(args.reference), configured_size)
next_cap = cap * (2 if configured_size == "xlarge" else 1)
if spent + next_cap > args.max_gpu_seconds:
print("Stopped at the experiment's GPU budget. This is not a measurement of platform quota.")
break
prompt = args.prompt or trial.get("prompt") or EXAMPLE_PROMPTS[trial["prompt_index"]]
params = [prompt, handle_file(str(args.reference)) if args.reference else None,
trial["quality"], trial["resolution"], trial["aspect"],
args.transparent or (trial["prompt_index"] == 3), trial["seed"], False]
row = {"trial_index": index, "trial": trial, "status": "started",
"started_at": datetime.now(timezone.utc).isoformat()}
started = time.perf_counter()
try:
response = client.predict(*params, api_name="/generate")
row["client_wall_seconds"] = time.perf_counter() - started
if not isinstance(response, (list, tuple)) or len(response) != 4:
raise ValueError("Expected image, status, recipe and seed from /generate")
image_src, status, recipe_src, returned_seed = response
image_dest = out / f"{index:03d}.png"
recipe_dest = out / f"{index:03d}.json"
shutil.copyfile(file_path(image_src), image_dest)
shutil.copyfile(file_path(recipe_src), recipe_dest)
recipe = json.loads(recipe_dest.read_text())
with Image.open(image_dest) as img:
img.load()
expected = SIZES[trial["resolution"]][trial["aspect"]]
if img.format != "PNG" or img.size != expected:
raise ValueError("Output format or dimensions do not match the requested image")
pixel_hash = hashlib.sha256(img.mode.encode() + str(img.size).encode() + img.tobytes()).hexdigest()
alpha_extrema = img.getchannel("A").getextrema() if "A" in img.getbands() else None
if returned_seed != trial["seed"] or recipe["parameters"]["seed"] != trial["seed"]:
raise ValueError("Returned seed does not match the fixed request")
if sha256(image_dest) != recipe["output"]["png_sha256"]:
raise ValueError("Downloaded PNG hash does not match the server recipe")
is_first = recipe["request_index_in_boot"] == 1
if args.cold_after_restart and index == 0 and not is_first:
raise ValueError("Cold-start assertion failed: this was not the app's first GPU request")
weight = 2 if recipe["metrics"]["gpu"]["size"] == "xlarge" else 1
spent += recipe["metrics"]["gpu_call_wall_seconds"] * weight
row.update(status="ok", recipe=recipe, image_file=image_dest.name,
recipe_file=recipe_dest.name, png_sha256=sha256(image_dest),
pixel_sha256=pixel_hash, alpha_extrema=alpha_extrema,
classification="first_after_observed_restart" if args.cold_after_restart and is_first
else "first_request_in_boot" if is_first
else "reused_gpu_worker" if recipe["metrics"].get("gpu_worker_call_index", 1) > 1
else "new_gpu_worker" if "gpu_worker_call_index" in recipe["metrics"]
else "subsequent_request_worker_warmth_unknown")
print(f"{index:03d} {trial['quality']} {trial['resolution']} "
f"inference={recipe['metrics']['inference_seconds']:.2f}s "
f"client={row['client_wall_seconds']:.2f}s")
except Exception as exc:
row.update(status="failed", client_wall_seconds=time.perf_counter() - started,
error_type=type(exc).__name__)
# Keep sensitive HTTP headers and credential-bearing traces out of artifacts.
print(f"Trial failed: {type(exc).__name__}. Inspect Space logs; no automatic retry.")
records.append(row)
with (out / "results.jsonl").open("a") as file:
file.write(json.dumps(row, ensure_ascii=False) + "\n")
if row["status"] == "failed":
break
print(f"Actual completed records: {len(records)}. Evidence: {out}")
if not records or any(row["status"] == "failed" for row in records):
sys.exit(1)
if __name__ == "__main__":
main()
|