Spaces:
Sleeping
Sleeping
Download scripts/run_matrix.py from BeardedAmbivert/inference-server: direct link, hf CLI and curl.
- Browser
- Download file 11.3 kB
-
https://huggingface.co/spaces/BeardedAmbivert/inference-server/resolve/main/scripts/run_matrix.py
- Command line
-
hf download hf://spaces/BeardedAmbivert/inference-server/scripts/run_matrix.py
-
curl -L -o run_matrix.py https://huggingface.co/spaces/BeardedAmbivert/inference-server/resolve/main/scripts/run_matrix.py
11.3 kB
| """Driver for the benchmark run matrices. | |
| Each server run needs a fresh uvicorn process because device, backend, batch size, and the | |
| ONNX model file are read once at startup (app/config.py), so they cannot be changed in a | |
| running server. For every config this script starts a server with the right env vars, waits | |
| for /health, runs scripts/bench.py against it, then tears the server down. Naive baseline runs | |
| skip the server and call scripts/naive_bench.py directly. | |
| Groups: | |
| legacy - the original 16 synthetic-input runs (1 short text/request, batch-size sweep). | |
| latency - nfcorpus *queries* (short), texts/request 1/8/32 at concurrency 1. | |
| throughput - nfcorpus *corpus* docs (long, mixed), encode batch 128/256/512 (concurrency = batch). | |
| The latency/throughput groups run across 4 backends: pytorch-cpu, pytorch-mps, onnx-fp32, onnx-int8. | |
| Usage: | |
| uv run python scripts/run_matrix.py # default: new = latency + throughput | |
| uv run python scripts/run_matrix.py --group all # legacy + latency + throughput | |
| uv run python scripts/run_matrix.py --group throughput --filter onnx-int8 | |
| uv run python scripts/run_matrix.py --dry-run # print the plan, run nothing | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import os | |
| import subprocess | |
| import time | |
| import urllib.error | |
| import urllib.request | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| REPO_ROOT = Path(__file__).resolve().parent.parent | |
| BENCHMARKS_DIR = REPO_ROOT / "benchmarks" | |
| DATA_DIR = BENCHMARKS_DIR / "data" | |
| ONNX_MODEL_DIR = REPO_ROOT / "models" / "minilm-onnx" | |
| HOST = "127.0.0.1" | |
| PORT = 8000 | |
| HEALTH_URL = f"http://{HOST}:{PORT}/health" | |
| EMBED_URL = f"http://{HOST}:{PORT}/embed" | |
| HEALTH_TIMEOUT_S = 120.0 | |
| QUERIES_FILE = "benchmarks/data/nfcorpus-queries.jsonl" # short -> latency | |
| CORPUS_FILE = "benchmarks/data/nfcorpus-corpus.jsonl" # long/mixed -> throughput | |
| # (label, backend, device, onnx_file_name) for the latency/throughput sweeps. | |
| WORKLOAD_BACKENDS = [ | |
| ("pytorch-cpu", "pytorch", "cpu", None), | |
| ("pytorch-mps", "pytorch", "mps", None), | |
| ("onnx-fp32", "onnx", "cpu", "onnx/model_O3.onnx"), | |
| ("onnx-int8", "onnx", "cpu", "onnx/model_int8.onnx"), | |
| ] | |
| class Run: | |
| name: str # output file stem / bench label | |
| group: str # "legacy" | "latency" | "throughput" | |
| script: str # "bench" (server) or "naive" (no server) | |
| backend: str # "pytorch" | "onnx" for server runs; "" for naive | |
| device: str # "cpu" | "mps" (metadata label; ONNX always runs on CPU) | |
| batch: int | None # server MAX_BATCH_SIZE; None for naive | |
| concurrency: int # bench concurrency; 1 for naive | |
| max_wait_ms: int = 500 # server MAX_WAIT_MS (batch time-trigger) | |
| texts_per_request: int = 1 | |
| text_source: str = "synthetic" # "synthetic" | "file" | |
| text_file: str | None = None # JSONL pool path (relative to repo root) when source=file | |
| onnx_file_name: str | None = None # ONNX_FILE_NAME (fp32 vs int8) for onnx runs | |
| requests: int = 500 | |
| warmup: int = 50 | |
| def build_legacy_runs() -> list[Run]: | |
| runs = [ | |
| Run("naive-cpu", "legacy", "naive", "", "cpu", None, 1), | |
| Run("naive-mps", "legacy", "naive", "", "mps", None, 1), | |
| Run("pytorch-cpu-batch1-c1", "legacy", "bench", "pytorch", "cpu", 1, 1), | |
| Run("pytorch-mps-batch1-c1", "legacy", "bench", "pytorch", "mps", 1, 1), | |
| ] | |
| for batch in (1, 8, 16, 32): | |
| runs.append(Run(f"pytorch-cpu-batch{batch}-c32", "legacy", "bench", "pytorch", "cpu", batch, 32)) | |
| for batch in (1, 8, 16, 32): | |
| runs.append(Run(f"pytorch-mps-batch{batch}-c32", "legacy", "bench", "pytorch", "mps", batch, 32)) | |
| for batch in (1, 8, 16, 32): | |
| runs.append(Run(f"onnx-cpu-batch{batch}-c32", "legacy", "bench", "onnx", "cpu", batch, 32)) | |
| return runs | |
| def build_latency_runs() -> list[Run]: | |
| runs = [] | |
| for label, backend, device, onnx_file in WORKLOAD_BACKENDS: | |
| for tpr in (1, 8, 32): | |
| runs.append(Run( | |
| name=f"lat-{label}-tpr{tpr}-c1", | |
| group="latency", script="bench", backend=backend, device=device, | |
| # batch=32 so a request's 8/32 texts encode in one pass; tiny max_wait_ms so | |
| # the batcher's time-trigger doesn't add latency waiting for c=1 requests. | |
| batch=32, concurrency=1, max_wait_ms=5, texts_per_request=tpr, | |
| text_source="file", text_file=QUERIES_FILE, onnx_file_name=onnx_file, | |
| requests=200, warmup=20, | |
| )) | |
| return runs | |
| def build_throughput_runs() -> list[Run]: | |
| runs = [] | |
| for label, backend, device, onnx_file in WORKLOAD_BACKENDS: | |
| for bs in (128, 256, 512): | |
| runs.append(Run( | |
| name=f"tput-{label}-bs{bs}", | |
| group="throughput", script="bench", backend=backend, device=device, | |
| batch=bs, concurrency=bs, texts_per_request=1, | |
| text_source="file", text_file=CORPUS_FILE, onnx_file_name=onnx_file, | |
| requests=4 * bs, warmup=bs, | |
| )) | |
| return runs | |
| def build_runs(group: str) -> list[Run]: | |
| groups = { | |
| "legacy": build_legacy_runs, | |
| "latency": build_latency_runs, | |
| "throughput": build_throughput_runs, | |
| } | |
| if group == "all": | |
| selected = ["legacy", "latency", "throughput"] | |
| elif group == "new": | |
| selected = ["latency", "throughput"] | |
| else: | |
| selected = [group] | |
| runs: list[Run] = [] | |
| for name in selected: | |
| runs.extend(groups[name]()) | |
| return runs | |
| def output_path(run: Run) -> Path: | |
| return BENCHMARKS_DIR / f"{run.name}.json" | |
| def start_server(run: Run) -> subprocess.Popen: | |
| env = os.environ.copy() | |
| env["MAX_BATCH_SIZE"] = str(run.batch) | |
| env["MAX_WAIT_MS"] = str(run.max_wait_ms) | |
| if run.backend == "onnx": | |
| env["BACKEND"] = "onnx" # ONNX path ignores DEVICE and runs on CPU | |
| if run.onnx_file_name: | |
| env["ONNX_FILE_NAME"] = run.onnx_file_name | |
| else: | |
| env.pop("BACKEND", None) | |
| env.pop("ONNX_FILE_NAME", None) | |
| env["DEVICE"] = run.device | |
| return subprocess.Popen( | |
| ["uv", "run", "uvicorn", "app.main:app", "--host", HOST, "--port", str(PORT), "--no-access-log"], | |
| cwd=REPO_ROOT, | |
| env=env, | |
| ) | |
| def wait_for_health(proc: subprocess.Popen, timeout_s: float) -> None: | |
| deadline = time.monotonic() + timeout_s | |
| while time.monotonic() < deadline: | |
| if proc.poll() is not None: | |
| raise RuntimeError(f"server exited during startup (code {proc.returncode})") | |
| try: | |
| with urllib.request.urlopen(HEALTH_URL, timeout=5) as resp: | |
| if resp.status == 200: | |
| return | |
| except (urllib.error.URLError, OSError): | |
| pass | |
| time.sleep(1.0) | |
| raise TimeoutError(f"server did not become healthy within {timeout_s:.0f}s") | |
| def stop_server(proc: subprocess.Popen) -> None: | |
| proc.terminate() | |
| try: | |
| proc.wait(timeout=15) | |
| except subprocess.TimeoutExpired: | |
| proc.kill() | |
| proc.wait() | |
| def run_bench(run: Run) -> None: | |
| cmd = [ | |
| "uv", "run", "python", "scripts/bench.py", | |
| "--label", run.name, | |
| "--backend", run.backend, | |
| "--device", run.device, | |
| "--server-batch-size", str(run.batch), | |
| "--concurrency", str(run.concurrency), | |
| "--requests", str(run.requests), | |
| "--warmup", str(run.warmup), | |
| "--texts-per-request", str(run.texts_per_request), | |
| "--text-source", run.text_source, | |
| "--url", EMBED_URL, | |
| "--output", str(output_path(run)), | |
| ] | |
| if run.text_source == "file": | |
| cmd += ["--text-file", run.text_file] | |
| subprocess.run(cmd, cwd=REPO_ROOT, check=True) | |
| def run_naive(run: Run) -> None: | |
| subprocess.run( | |
| [ | |
| "uv", "run", "python", "scripts/naive_bench.py", | |
| "--label", run.name, | |
| "--device", run.device, | |
| "--requests", str(run.requests), | |
| "--warmup", str(run.warmup), | |
| "--texts-per-request", str(run.texts_per_request), | |
| "--output", str(output_path(run)), | |
| ], | |
| cwd=REPO_ROOT, | |
| check=True, | |
| ) | |
| def execute(run: Run) -> None: | |
| if run.script == "naive": | |
| run_naive(run) | |
| return | |
| proc = start_server(run) | |
| try: | |
| wait_for_health(proc, HEALTH_TIMEOUT_S) | |
| run_bench(run) | |
| finally: | |
| stop_server(proc) | |
| time.sleep(1.0) # let the port free before the next server binds | |
| def print_plan(runs: list[Run]) -> None: | |
| print(f"Planned runs ({len(runs)}):") | |
| for i, run in enumerate(runs, 1): | |
| batch = "-" if run.batch is None else str(run.batch) | |
| print( | |
| f" {i:2d}. {run.name:24s} grp={run.group:10s} backend={run.backend or '-':7s} " | |
| f"device={run.device:3s} batch={batch:>3s} c={run.concurrency:<3d} " | |
| f"tpr={run.texts_per_request:<3d} src={run.text_source:9s} -> benchmarks/{run.name}.json" | |
| ) | |
| def validate_data_files(runs: list[Run]) -> None: | |
| needed = {run.text_file for run in runs if run.text_source == "file"} | |
| missing = [f for f in needed if f and not (REPO_ROOT / f).exists()] | |
| if missing: | |
| raise SystemExit( | |
| f"missing dataset pools: {missing}. Run `uv run python scripts/prepare_dataset.py` first." | |
| ) | |
| if any(r.backend == "onnx" for r in runs) and not ONNX_MODEL_DIR.exists(): | |
| raise SystemExit( | |
| f"ONNX runs selected but {ONNX_MODEL_DIR} is missing. " | |
| f"Run `uv run python scripts/export_onnx.py` first, or use --filter to skip onnx." | |
| ) | |
| int8_needed = any(r.onnx_file_name == "onnx/model_int8.onnx" for r in runs) | |
| if int8_needed and not (ONNX_MODEL_DIR / "onnx" / "model_int8.onnx").exists(): | |
| raise SystemExit( | |
| "onnx-int8 runs selected but onnx/model_int8.onnx is missing. " | |
| "Run `uv run python scripts/quantize_onnx.py` first, or use --filter to skip int8." | |
| ) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Run the benchmark matrices.") | |
| parser.add_argument( | |
| "--group", choices=("legacy", "latency", "throughput", "new", "all"), default="new", | |
| help="Which run group(s). 'new' = latency + throughput (default); 'all' adds legacy.", | |
| ) | |
| parser.add_argument("--dry-run", action="store_true", help="Print the planned runs and exit.") | |
| parser.add_argument("--filter", help="Only run configs whose name contains this substring.") | |
| args = parser.parse_args() | |
| runs = build_runs(args.group) | |
| if args.filter: | |
| runs = [r for r in runs if args.filter in r.name] | |
| if not runs: | |
| raise SystemExit(f"no runs match filter {args.filter!r}") | |
| print_plan(runs) | |
| if args.dry_run: | |
| return | |
| validate_data_files(runs) | |
| BENCHMARKS_DIR.mkdir(parents=True, exist_ok=True) | |
| for i, run in enumerate(runs, 1): | |
| print(f"\n=== [{i}/{len(runs)}] {run.name} ===", flush=True) | |
| execute(run) | |
| print("\nMatrix complete.") | |
| if __name__ == "__main__": | |
| main() | |