gliner2-5-multi-coreml / verify-compression.py
alexwengg's picture
Add validated asymmetric embedding W8 classification option
b172fa5 verified
Raw
History Blame Contribute Delete
6.07 kB
"""Compare two GLiNER2 Core ML packages on selected real classification requests."""
from __future__ import annotations
import argparse
import json
import math
import platform
import statistics
import subprocess
import time
from pathlib import Path
import coremltools as ct
import numpy as np
from preprocessing import load_processor, prepare_with_processor
def percentile(values: list[float], p: float) -> float | None:
if not values:
return None
return sorted(values)[max(0, min(len(values) - 1, math.ceil(p * len(values)) - 1))]
def median(cases: list[dict], field: str) -> float | None:
return statistics.median(row[field] for row in cases) if cases else None
def run() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--reference", type=Path, required=True)
parser.add_argument("--candidate", type=Path, required=True)
parser.add_argument("--tokenizer", type=Path, required=True)
parser.add_argument("--manifest", type=Path, required=True)
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--compute-units", choices=["all", "cpu-ane"], default="all")
parser.add_argument("--length", type=int, default=128)
parser.add_argument("--max-options", type=int, default=8)
parser.add_argument("--limit", type=int, default=None, help="stop after this many eligible requests")
parser.add_argument("--max-probability-error", type=float, default=0.02)
args = parser.parse_args()
units = ct.ComputeUnit.ALL if args.compute_units == "all" else ct.ComputeUnit.CPU_AND_NE
processor = load_processor(str(args.tokenizer))
load_start = time.perf_counter()
reference = ct.models.MLModel(str(args.reference), compute_units=units)
reference_load_ms = (time.perf_counter() - load_start) * 1000
load_start = time.perf_counter()
candidate = ct.models.MLModel(str(args.candidate), compute_units=units)
candidate_load_ms = (time.perf_counter() - load_start) * 1000
rows = [json.loads(line) for line in args.manifest.read_text().splitlines() if line]
prepared = []
skipped = []
for row in rows:
if args.limit is not None and len(prepared) >= args.limit:
break
labels = [(description or key).strip() for key, description in row["options"]]
prepare_start = time.perf_counter()
try:
arrays = prepare_with_processor(processor, row["state"], "decision", labels, args.length, args.max_options)
except ValueError as error:
skipped.append({"suite": row["suite"], "index": row["index"], "reason": str(error)})
continue
prepare_ms = (time.perf_counter() - prepare_start) * 1000
prepared.append((row, labels, arrays, prepare_ms))
if prepared:
for model in (reference, candidate):
model.predict(prepared[0][2])
checked = []
for index, (row, labels, arrays, prepare_ms) in enumerate(prepared):
outputs = {}
order = (("reference", reference), ("candidate", candidate))
if index % 2:
order = tuple(reversed(order))
for name, model in order:
start = time.perf_counter()
probabilities = np.asarray(model.predict(arrays)["probabilities"])[0, : len(labels)]
outputs[name] = (probabilities, (time.perf_counter() - start) * 1000)
expected, reference_ms = outputs["reference"]
actual, candidate_ms = outputs["candidate"]
checked.append({
"suite": row["suite"],
"index": row["index"],
"choice_agrees": int(expected.argmax()) == int(actual.argmax()),
"max_probability_error": float(np.max(np.abs(expected - actual))),
"prepare_ms": prepare_ms,
"reference_ms": reference_ms,
"candidate_ms": candidate_ms,
"reference_total_ms": prepare_ms + reference_ms,
"candidate_total_ms": prepare_ms + candidate_ms,
})
report = {
"reference": str(args.reference),
"candidate": str(args.candidate),
"manifest": str(args.manifest),
"compute_units": args.compute_units,
"processor": platform.processor(),
"machine": platform.machine(),
"macos": platform.mac_ver()[0],
"power_audit_after_run": subprocess.run(["pmset", "-g", "batt"], capture_output=True, text=True).stdout.strip(),
"reference_load_ms": reference_load_ms,
"candidate_load_ms": candidate_load_ms,
"warmup_calls_per_model": 1 if prepared else 0,
"timed_order": "alternating reference-first/candidate-first by request",
"checked": len(checked),
"skipped": skipped,
"choice_agreement": sum(row["choice_agrees"] for row in checked),
"max_probability_error": max((row["max_probability_error"] for row in checked), default=None),
"probability_error_limit": args.max_probability_error,
"reference_p50_ms": median(checked, "reference_ms"),
"candidate_p50_ms": median(checked, "candidate_ms"),
"reference_p95_ms": percentile([row["reference_ms"] for row in checked], 0.95),
"candidate_p95_ms": percentile([row["candidate_ms"] for row in checked], 0.95),
"reference_total_p50_ms": median(checked, "reference_total_ms"),
"candidate_total_p50_ms": median(checked, "candidate_total_ms"),
"cases": checked,
}
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(report, indent=2) + "\n")
summary = {key: value for key, value in report.items() if key not in ("cases", "skipped")}
summary["skipped_count"] = len(skipped)
print(json.dumps(summary, indent=2))
if not checked or report["choice_agreement"] != len(checked):
raise SystemExit("candidate changed a checked decision")
if report["max_probability_error"] > args.max_probability_error:
raise SystemExit("candidate exceeded the selected-request probability error limit")
if __name__ == "__main__":
run()