"""Orchestrate the quantization sweep: baseline -> each method -> aggregate -> plot. Each method is isolated: any exception becomes a status="failed" row so one bad method never aborts the sweep. """ from __future__ import annotations import argparse import json from pathlib import Path from typing import Any, Dict, List from scripts.quant.aggregate import build_row, rows_to_csv, rows_to_markdown from scripts.quant.bbox_iou import bbox_iou_over_dirs from scripts.quant.build_model import build_model from scripts.quant.capture import capture_page from scripts.quant.manifest import load_manifest from scripts.quant.recipes import METHOD_SPECS, method_names, vllm_serve_args from scripts.quant.serve import start_server, stop_server, wait_healthy # Reuse the existing CER metric. from scripts.cer_divergence import cer_over_dirs OCR_URL = "http://127.0.0.1:5002/v1/api/ai/suya_ocr_vllm/" def _dir_size_mb(path: Path) -> float: total = sum(f.stat().st_size for f in path.rglob("*") if f.is_file()) return round(total / (1024 * 1024), 1) def _measure_method(method: str, base_model: str, work_dir: Path, eval_images: List[Path], reference_dir: Path) -> Dict[str, Any]: """Build -> serve -> capture -> metrics for one method. Raises on any failure. Assumes the API process (api.py) is already running and reads SURYA_INFERENCE_URL from the env to point at the per-method vLLM server (port set by the runbook). Returns a dict of measured summary fields (no method/status/t4 keys). """ model_dir = work_dir / "models" / method out_capture = work_dir / "results" / method port = 8000 built = build_model(method, base_model, model_dir, eval_images[: min(8, len(eval_images))]) serve_args = vllm_serve_args(method, str(built), base_model, port) proc = start_server(serve_args, str(work_dir / f"vllm_{method}.log")) try: if not wait_healthy(port): raise RuntimeError(f"vLLM did not become healthy for {method}") latencies = [] for image in eval_images: cap = capture_page(OCR_URL, image, out_capture, timeout=900) if cap["elapsed_seconds"] is not None: latencies.append(cap["elapsed_seconds"]) cer = cer_over_dirs(reference_dir, out_capture) iou = bbox_iou_over_dirs(reference_dir, out_capture) finally: stop_server(port, proc) latencies.sort() n = len(latencies) size = _dir_size_mb(built) if METHOD_SPECS[method]["kind"] in ("compressor", "bnb") else None return { "mean_latency_s": round(sum(latencies) / n, 3) if n else None, "p50_latency_s": latencies[n // 2] if n else None, "p95_latency_s": latencies[min(n - 1, int(n * 0.95))] if n else None, "mean_cer": cer["mean_cer"], "max_cer": cer["max_cer"], "mean_bbox_iou": iou["mean_bbox_iou"], "mean_missed_lines": iou["mean_missed_lines"], "mean_extra_lines": iou["mean_extra_lines"], "model_size_mb": size, } def run_method(method: str, base_model: str, work_dir: Path, eval_images: List[Path], reference_dir: Path) -> Dict[str, Any]: t4 = METHOD_SPECS[method]["t4_deployable"] try: metrics = _measure_method(method, base_model, work_dir, eval_images, reference_dir) return build_row(method=method, status="ok", t4_deployable=t4, **metrics) except Exception as exc: # feasibility probe: record and continue return build_row(method=method, status="failed", t4_deployable=t4, error=str(exc)) def main() -> None: parser = argparse.ArgumentParser(description="Run the quantization benchmark sweep.") parser.add_argument("--base-model", default="datalab-to/surya-ocr-2") parser.add_argument("--manifest", type=Path, default=Path("eval_set/manifest.txt")) parser.add_argument("--image-root", type=Path, default=Path(".")) parser.add_argument("--work-dir", type=Path, default=Path("results/quant")) parser.add_argument("--methods", nargs="*", default=method_names()) args = parser.parse_args() eval_images = load_manifest(args.manifest, args.image_root) reference_dir = args.work_dir / "reference" args.work_dir.mkdir(parents=True, exist_ok=True) rows = [] for method in args.methods: print(f"=== {method} ===", flush=True) rows.append(run_method(method, args.base_model, args.work_dir, eval_images, reference_dir)) (args.work_dir / "summary.csv").write_text(rows_to_csv(rows), encoding="utf-8") (args.work_dir / "summary.json").write_text(json.dumps(rows, indent=2), encoding="utf-8") (args.work_dir / "summary.md").write_text(rows_to_markdown(rows), encoding="utf-8") print(f"wrote summary for {len(rows)} methods to {args.work_dir}") if __name__ == "__main__": main()