Remove unused imports, fix unsorted imports, remove bare f-strings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
197 lines
5.9 KiB
Python
197 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
winnow inference benchmark: GPU vs CPU throughput.
|
||
|
||
Measures InsightFace (face mode) and SigLIP (object mode) latency and
|
||
throughput. Run with FORCE_CPU=true for CPU-only baseline.
|
||
|
||
Usage inside container:
|
||
# GPU mode:
|
||
docker exec winnow python /app/scripts/benchmark.py
|
||
|
||
# CPU mode:
|
||
docker exec -e FORCE_CPU=true winnow python /app/scripts/benchmark.py
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import time
|
||
|
||
import numpy as np
|
||
from PIL import Image, ImageDraw
|
||
|
||
|
||
def _mode_label() -> str:
|
||
if os.getenv("FORCE_CPU", "").lower() in ("true", "1", "yes"):
|
||
return "CPU (FORCE_CPU=true)"
|
||
return "GPU (auto)"
|
||
|
||
|
||
def make_face_image(size: int = 640) -> Image.Image:
|
||
"""Synthetic face-like image: skin-tone rectangle with landmark blobs."""
|
||
img = Image.new("RGB", (size, size), (200, 170, 140))
|
||
draw = ImageDraw.Draw(img)
|
||
# Head oval
|
||
cx, cy = size // 2, size // 2
|
||
hw, hh = int(size * 0.3), int(size * 0.38)
|
||
draw.ellipse([cx - hw, cy - hh, cx + hw, cy + hh], fill=(220, 185, 155))
|
||
# Eyes
|
||
for ex in [cx - int(size * 0.1), cx + int(size * 0.1)]:
|
||
ey = cy - int(size * 0.05)
|
||
r = max(4, size // 40)
|
||
draw.ellipse([ex - r, ey - r, ex + r, ey + r], fill=(40, 30, 20))
|
||
# Nose
|
||
draw.ellipse([cx - 5, cy + 5, cx + 5, cy + 15], fill=(180, 140, 110))
|
||
# Mouth
|
||
draw.arc([cx - 20, cy + 25, cx + 20, cy + 45], start=0, end=180, fill=(160, 80, 80), width=3)
|
||
return img
|
||
|
||
|
||
def make_random_image(width: int = 224, height: int = 224) -> Image.Image:
|
||
rng = np.random.default_rng(42)
|
||
return Image.fromarray(rng.integers(0, 256, (height, width, 3), dtype=np.uint8), "RGB")
|
||
|
||
|
||
def _stats(times_s: list[float]) -> dict:
|
||
arr = np.array(times_s) * 1000 # ms
|
||
return {
|
||
"median_ms": float(np.median(arr)),
|
||
"mean_ms": float(np.mean(arr)),
|
||
"min_ms": float(np.min(arr)),
|
||
"p95_ms": float(np.percentile(arr, 95)),
|
||
"ips": 1000.0 / float(np.median(arr)),
|
||
}
|
||
|
||
|
||
def bench_insightface(n_warmup: int = 5, n_runs: int = 30) -> None:
|
||
import cv2
|
||
|
||
import winnow.embeddings as emb_mod
|
||
from winnow.embeddings import get_insightface_app
|
||
|
||
# Reset singleton so we get a fresh load
|
||
emb_mod._insightface_app = None
|
||
emb_mod._insightface_loaded = False
|
||
|
||
print(" Loading model...")
|
||
t_load = time.perf_counter()
|
||
app = get_insightface_app()
|
||
load_s = time.perf_counter() - t_load
|
||
|
||
if app is None:
|
||
print(" SKIP: InsightFace failed to load")
|
||
return
|
||
|
||
img_pil = make_face_image(640)
|
||
img_bgr = cv2.cvtColor(np.asarray(img_pil), cv2.COLOR_RGB2BGR)
|
||
|
||
# Warmup
|
||
for _ in range(n_warmup):
|
||
app.get(img_bgr)
|
||
|
||
# Timed — single image 640×640
|
||
times: list[float] = []
|
||
for _ in range(n_runs):
|
||
t0 = time.perf_counter()
|
||
app.get(img_bgr)
|
||
times.append(time.perf_counter() - t0)
|
||
|
||
s = _stats(times)
|
||
print(f" Model load time : {load_s:.2f} s")
|
||
print(" Input size : 640×640")
|
||
print(f" Runs : {n_runs} (after {n_warmup} warmup)")
|
||
print(f" Median latency : {s['median_ms']:.1f} ms")
|
||
print(f" Mean / p95 : {s['mean_ms']:.1f} ms / {s['p95_ms']:.1f} ms")
|
||
print(f" Min latency : {s['min_ms']:.1f} ms")
|
||
print(f" Throughput : {s['ips']:.1f} images/s")
|
||
|
||
# Also test at 320×320
|
||
img_sm = make_face_image(320)
|
||
img_sm_bgr = cv2.cvtColor(np.asarray(img_sm), cv2.COLOR_RGB2BGR)
|
||
for _ in range(n_warmup):
|
||
app.get(img_sm_bgr)
|
||
times_sm: list[float] = []
|
||
for _ in range(n_runs):
|
||
t0 = time.perf_counter()
|
||
app.get(img_sm_bgr)
|
||
times_sm.append(time.perf_counter() - t0)
|
||
s2 = _stats(times_sm)
|
||
print(f" 320×320 median : {s2['median_ms']:.1f} ms ({s2['ips']:.1f} img/s)")
|
||
|
||
|
||
def bench_siglip(
|
||
n_warmup: int = 3,
|
||
n_runs: int = 20,
|
||
batch_sizes: tuple = (1, 4, 8, 16, 32),
|
||
) -> None:
|
||
import torch
|
||
|
||
import winnow.embeddings as emb_mod
|
||
emb_mod._siglip_model = None
|
||
emb_mod._siglip_processor = None
|
||
emb_mod._siglip_loaded = False
|
||
|
||
print(" Loading model...")
|
||
t_load = time.perf_counter()
|
||
model, processor = emb_mod.get_siglip_model()
|
||
load_s = time.perf_counter() - t_load
|
||
|
||
if model is None:
|
||
print(" SKIP: SigLIP failed to load")
|
||
return
|
||
|
||
device = next(model.parameters()).device
|
||
print(f" Model load time : {load_s:.2f} s (device: {device})")
|
||
|
||
print(f" {'Batch':>5} {'ms/batch':>10} {'ms/img':>8} {'img/s':>8} {'p95/img':>9}")
|
||
for bs in batch_sizes:
|
||
imgs = [make_random_image(224, 224) for _ in range(bs)]
|
||
inputs = processor(images=imgs, return_tensors="pt")
|
||
inputs = {k: v.to(device) for k, v in inputs.items()}
|
||
|
||
# Warmup
|
||
for _ in range(n_warmup):
|
||
with torch.no_grad():
|
||
model(**inputs)
|
||
if str(device) != "cpu":
|
||
torch.cuda.synchronize()
|
||
|
||
times: list[float] = []
|
||
for _ in range(n_runs):
|
||
if str(device) != "cpu":
|
||
torch.cuda.synchronize()
|
||
t0 = time.perf_counter()
|
||
with torch.no_grad():
|
||
model(**inputs)
|
||
if str(device) != "cpu":
|
||
torch.cuda.synchronize()
|
||
times.append(time.perf_counter() - t0)
|
||
|
||
s = _stats(times)
|
||
print(
|
||
f" {bs:>5} {s['median_ms']:>10.1f} {s['median_ms']/bs:>8.2f}"
|
||
f" {bs * 1000 / s['median_ms']:>8.1f} {s['p95_ms']/bs:>9.2f}"
|
||
)
|
||
|
||
|
||
def main() -> None:
|
||
print("=" * 56)
|
||
print(" winnow inference benchmark")
|
||
print(f" Mode: {_mode_label()}")
|
||
print("=" * 56)
|
||
print()
|
||
|
||
print("── InsightFace Buffalo_L (face detection + ArcFace) ──")
|
||
bench_insightface()
|
||
print()
|
||
|
||
print("── SigLIP google/siglip-base-patch16-224 (objects) ───")
|
||
bench_siglip()
|
||
print()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# Add winnow to path when run directly inside container
|
||
sys.path.insert(0, "/app")
|
||
main()
|