Spaces:
Sleeping
Sleeping
| """Benchmark BLIP caption latency + peak memory on CPU (Phase 5, killer gate #3). | |
| No published CPU latency exists for blip-image-captioning-base; this measures it directly to | |
| decide whether it ships as-is on the 2-vCPU HF Space, needs int8 quantization, or a fallback. | |
| Standalone — no capit import; the backend is its own component. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import resource | |
| import time | |
| from pathlib import Path | |
| import torch | |
| from PIL import Image, ImageOps | |
| from transformers import BlipForConditionalGeneration, BlipProcessor | |
| MODEL = "Salesforce/blip-image-captioning-base" | |
| _REPO_ROOT = Path(__file__).resolve().parents[2] | |
| def _test_images(data_root: Path, n: int) -> list[Path]: | |
| records = json.loads((data_root / "dataset_flickr8k.json").read_text())["images"] | |
| test = sorted((r for r in records if r["split"] == "test"), key=lambda r: r["filename"]) | |
| return [data_root / "Images" / r["filename"] for r in test[:n]] | |
| def _peak_rss_mb() -> float: | |
| return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024 # ru_maxrss is KB on Linux | |
| def _pct(xs: list[float], q: float) -> float: | |
| xs = sorted(xs) | |
| k = (len(xs) - 1) * q | |
| lo = int(k) | |
| hi = min(lo + 1, len(xs) - 1) | |
| return xs[lo] + (xs[hi] - xs[lo]) * (k - lo) | |
| def bench(processor, model, paths: list[Path], num_beams: int) -> dict: | |
| latencies: list[float] = [] | |
| sample = "" | |
| for i, p in enumerate(paths): | |
| with Image.open(p) as im: | |
| image = (ImageOps.exif_transpose(im) or im).convert("RGB") | |
| t0 = time.perf_counter() | |
| inputs = processor(image, return_tensors="pt") | |
| out = model.generate(**inputs, num_beams=num_beams, max_new_tokens=30) | |
| dt = time.perf_counter() - t0 | |
| if i == 0: # warm-up: first call pays lazy init / caching | |
| sample = processor.decode(out[0], skip_special_tokens=True) | |
| continue | |
| latencies.append(dt) | |
| return { | |
| "num_beams": num_beams, | |
| "n": len(latencies), | |
| "mean_s": sum(latencies) / len(latencies), | |
| "p95_s": _pct(latencies, 0.95), | |
| "min_s": min(latencies), | |
| "max_s": max(latencies), | |
| "sample_caption": sample, | |
| } | |
| def _verdict(mean_s: float) -> str: | |
| if mean_s <= 5.0: | |
| return "SHIP AS-IS (comfortable margin under the 10s Space budget)" | |
| if mean_s <= 10.0: | |
| return "TIGHT — under budget locally but little Space margin; consider int8 quantization" | |
| return "MISS — exceeds budget; int8 quantize (OpenVINO/NNCF) then re-bench, else ViT-GPT2 fallback" | |
| def main() -> None: | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--data-root", default=str(_REPO_ROOT / "data" / "flickr8k")) | |
| parser.add_argument("--n", type=int, default=20) | |
| parser.add_argument("--threads", type=int, default=2, help="2 simulates the HF Space's 2 vCPU") | |
| parser.add_argument("--beams", type=int, nargs="+", default=[1, 3]) | |
| parser.add_argument("--out-json", default=str(_REPO_ROOT / "data" / "blip_bench.json")) | |
| args = parser.parse_args() | |
| torch.set_num_threads(args.threads) | |
| processor = BlipProcessor.from_pretrained(MODEL) | |
| model = BlipForConditionalGeneration.from_pretrained(MODEL).eval() | |
| paths = _test_images(Path(args.data_root), args.n + 1) # +1 for the discarded warm-up | |
| runs = [bench(processor, model, paths, b) for b in args.beams] | |
| peak_rss = _peak_rss_mb() | |
| headline = max(runs, key=lambda r: r["num_beams"]) # the slowest config is what the UI serves | |
| report = { | |
| "model": MODEL, | |
| "threads": args.threads, | |
| "torch_threads_available": torch.get_num_threads(), | |
| "peak_rss_mb": round(peak_rss, 1), | |
| "runs": runs, | |
| "headline_beam": headline["num_beams"], | |
| "headline_mean_s": round(headline["mean_s"], 2), | |
| "space_budget_s": 10.0, | |
| "verdict": _verdict(headline["mean_s"]), | |
| } | |
| Path(args.out_json).write_text(json.dumps(report, indent=2)) | |
| print(f"model: {MODEL} threads: {args.threads} peak RSS: {peak_rss:.0f} MB") | |
| for r in runs: | |
| print( | |
| f" beams={r['num_beams']} mean={r['mean_s']:.2f}s p95={r['p95_s']:.2f}s " | |
| f"min={r['min_s']:.2f}s max={r['max_s']:.2f}s (n={r['n']})" | |
| ) | |
| print(f' sample: "{runs[-1]["sample_caption"]}"') | |
| print(f"verdict (beams={headline['num_beams']}, {headline['mean_s']:.2f}s vs 10s budget): {report['verdict']}") | |
| print(f"wrote {args.out_json}") | |
| if __name__ == "__main__": | |
| main() | |