Spaces:
Sleeping
Sleeping
| """CPU inference latency benchmark for the two CV backbones. | |
| Motivation: the original model comparison claimed MobileNetV3 is | |
| "deployment-friendly / faster". Training throughput did not confirm that, so | |
| this script measures the metric that actually matters for the CPU-only | |
| HuggingFace Space: single-image forward-pass latency. It also reports the | |
| parameter count and on-disk model size. | |
| Usage: | |
| python -m src.cv.benchmark [--runs 100] [--warmup 10] | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[2])) | |
| from src.config import MODELS_DIR # noqa: E402 | |
| from src.cv.train import build_model # noqa: E402 | |
| LATENCY_REPORT = MODELS_DIR / "cv_latency_report.json" | |
| BACKBONES = ["resnet18", "mobilenet_v3_small"] | |
| DEFAULT_NUM_CLASSES = 10 | |
| def _param_count_millions(model: torch.nn.Module) -> float: | |
| return sum(p.numel() for p in model.parameters()) / 1e6 | |
| def _model_size_mb(model: torch.nn.Module) -> float: | |
| """Serialized state_dict size in MB (proxy for the deployed artifact).""" | |
| import io | |
| buffer = io.BytesIO() | |
| torch.save(model.state_dict(), buffer) | |
| return buffer.getbuffer().nbytes / (1024 * 1024) | |
| def benchmark_model( | |
| name: str, | |
| num_classes: int, | |
| runs: int, | |
| warmup: int, | |
| ) -> dict: | |
| torch.manual_seed(42) | |
| device = torch.device("cpu") | |
| model = build_model(name, num_classes).to(device).eval() | |
| dummy = torch.randn(1, 3, 224, 224, device=device) | |
| for _ in range(warmup): | |
| model(dummy) | |
| timings_ms: list[float] = [] | |
| for _ in range(runs): | |
| start = time.perf_counter() | |
| model(dummy) | |
| timings_ms.append((time.perf_counter() - start) * 1000.0) | |
| arr = np.array(timings_ms) | |
| return { | |
| "model": name, | |
| "params_m": round(_param_count_millions(model), 3), | |
| "size_mb": round(_model_size_mb(model), 2), | |
| "latency_ms_mean": round(float(arr.mean()), 2), | |
| "latency_ms_p50": round(float(np.percentile(arr, 50)), 2), | |
| "latency_ms_p95": round(float(np.percentile(arr, 95)), 2), | |
| "runs": runs, | |
| } | |
| def main() -> None: | |
| p = argparse.ArgumentParser(description=__doc__) | |
| p.add_argument("--runs", type=int, default=100) | |
| p.add_argument("--warmup", type=int, default=10) | |
| p.add_argument("--num-classes", type=int, default=DEFAULT_NUM_CLASSES) | |
| args = p.parse_args() | |
| torch.set_num_threads(max(1, torch.get_num_threads())) | |
| print(f"[cv.benchmark] CPU threads: {torch.get_num_threads()}") | |
| results = [ | |
| benchmark_model(name, args.num_classes, args.runs, args.warmup) | |
| for name in BACKBONES | |
| ] | |
| for r in results: | |
| print( | |
| f"[cv.benchmark] {r['model']:>20} params={r['params_m']:.2f}M " | |
| f"size={r['size_mb']:.1f}MB " | |
| f"latency_mean={r['latency_ms_mean']:.1f}ms " | |
| f"p95={r['latency_ms_p95']:.1f}ms" | |
| ) | |
| faster = min(results, key=lambda r: r["latency_ms_p95"]) | |
| payload = { | |
| "device": "cpu", | |
| "threads": torch.get_num_threads(), | |
| "results": results, | |
| "fastest_p95": faster["model"], | |
| } | |
| LATENCY_REPORT.write_text(json.dumps(payload, indent=2)) | |
| print(f"[cv.benchmark] wrote {LATENCY_REPORT}") | |
| if __name__ == "__main__": | |
| main() | |