viggle-animate-workflow / tools /time_checkpoints.py
xue
SlimDiT source: conversion library + ComfyUI node pack + workflows + tests
bf2a6c7 verified
Raw History Blame Contribute Delete
4.3 kB
"""Time the same workflow across several checkpoints, twice each (cold + warm), via the ComfyUI API.
python3 time_checkpoints.py --host 127.0.0.1 --port 8188 --workflow smoke-prompt.json \\
--checkpoints name1.safetensors name2.safetensors
Prints, per run: wall time (submit -> finished) and the server's own "Prompt executed" figure is
captured separately from the container log. The first run of each checkpoint includes loading it
from disk; the second is the steady state.
"""
from __future__ import annotations
import argparse
import json
import sys
import time
import urllib.parse
import urllib.request
import uuid
from pathlib import Path
def post(url: str, payload: dict) -> dict:
req = urllib.request.Request(url, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=120) as resp:
return json.loads(resp.read())
def get(url: str) -> dict:
with urllib.request.urlopen(url, timeout=60) as resp:
return json.loads(resp.read())
def run_once(base: str, graph: dict, checkpoint: str, prefix: str, timeout: float, seed: int | None = None) -> dict:
graph["13"]["inputs"]["unet_name"] = checkpoint
graph["21"]["inputs"]["filename_prefix"] = prefix
for node in graph.values():
if node.get("class_type") == "UNETLoader":
node["inputs"]["unet_name"] = checkpoint
if node.get("class_type") == "VHS_VideoCombine":
node["inputs"]["filename_prefix"] = prefix
if node.get("class_type") == "RandomNoise" and seed is not None:
# ComfyUI caches per-node results: an unchanged seed makes the whole sampler chain a
# cache hit (~0.7 s), so a *real* warm re-run needs a new noise seed.
node["inputs"]["noise_seed"] = seed
t0 = time.time()
started = post(f"{base}/prompt", {"prompt": graph, "client_id": str(uuid.uuid4())})["prompt_id"]
first_seen = None
while time.time() - t0 < timeout:
history = get(f"{base}/history/{started}")
entry = history.get(started)
if entry and entry.get("outputs"):
status = entry.get("status", {})
if status.get("status_str") != "success":
return {"ok": False, "error": json.dumps(status)[:200], "wall": time.time() - t0}
exec_ms = None
for name, payload in status.get("messages", []):
if name == "execution_start":
first_seen = payload.get("timestamp")
if name == "execution_success":
exec_ms = payload.get("timestamp", 0) - (first_seen or 0)
return {"ok": True, "wall": time.time() - t0, "exec": (exec_ms or 0) / 1000.0}
time.sleep(1)
return {"ok": False, "error": "timeout", "wall": time.time() - t0}
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--host", required=True)
ap.add_argument("--port", type=int, default=8188)
ap.add_argument("--workflow", required=True)
ap.add_argument("--checkpoints", nargs="+", required=True)
ap.add_argument("--repeats", type=int, default=2)
ap.add_argument("--seed-base", type=int, default=2347709, help="seed for run 1; run N uses base+N-1")
ap.add_argument("--timeout", type=float, default=900)
args = ap.parse_args(argv)
base = f"http://{args.host}:{args.port}"
graph = json.loads(Path(args.workflow).read_text())
graph = graph.get("prompt", graph)
print(f"{'checkpoint':<48s} {'run':>3s} {'seed':>9s} {'wall s':>8s} {'service s':>10s}")
for ckpt in args.checkpoints:
for i in range(1, args.repeats + 1):
prefix = f"timing/{Path(ckpt).stem[:28]}_{i}"
seed = args.seed_base + i - 1
result = run_once(base, json.loads(json.dumps(graph)), ckpt, prefix, args.timeout, seed=seed)
if result["ok"]:
print(f"{ckpt:<48s} {i:>3d} {seed:>9d} {result['wall']:>8.1f} {result['exec']:>10.1f}")
else:
print(f"{ckpt:<48s} {i:>3d} {seed:>9d} FAILED: {result['error']}")
sys.stdout.flush()
return 0
if __name__ == "__main__":
sys.exit(main())