alexwengg's picture
Publish GLiNER2.5 small Core ML classification exports
4e12f7c verified
Raw History Blame Contribute Delete
3.71 kB
"""Compare the pinned GLiNER2 native classifier with its Core ML export."""
import argparse
import json
import statistics
import time
from pathlib import Path
import coremltools as ct
import numpy as np
import torch
from gliner2 import AutoExtractor
from preprocessing import prepare_classification
MODEL_ID = "fastino/gliner2.5-small-v1"
MODEL_REVISION = "7e6f537f10337497069276892a5ef435028252ce"
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--package", default="build/gliner2_small_classification_fp16_L128_K8.mlpackage")
parser.add_argument("--suite", required=True)
parser.add_argument("--limit", type=int, default=100)
parser.add_argument("--length", type=int, default=128)
parser.add_argument("--max-options", type=int, default=8)
parser.add_argument("--out", default="build/verify.json")
args = parser.parse_args()
torch.set_num_threads(4)
source = Path.home() / ".cache/huggingface/hub/models--fastino--gliner2.5-small-v1/snapshots" / MODEL_REVISION
native = AutoExtractor.from_pretrained(str(source), map_location="cpu").eval()
model = ct.models.MLModel(args.package, compute_units=ct.ComputeUnit.ALL)
counts = {"checked": 0, "too_many_options": 0, "too_long": 0, "duplicate_labels": 0, "mismatches": 0}
errors = []
latencies = []
failures = []
for line in Path(args.suite).open():
row = json.loads(line)
options = row.get("options") or []
if len(options) > args.max_options or not options:
counts["too_many_options"] += 1
continue
labels = [(description or key).strip() for key, description in options]
if len(set(labels)) != len(labels):
counts["duplicate_labels"] += 1
continue
text = row["state"]
task = "decision"
try:
arrays = prepare_classification(native, text, task, labels, args.length, args.max_options)
except ValueError:
counts["too_long"] += 1
continue
expected = native.classify_text(text, {task: labels}, include_confidence=True, max_len=args.length)[task]
start = time.perf_counter()
prediction = model.predict(arrays)
latencies.append((time.perf_counter() - start) * 1000)
probabilities = np.asarray(prediction["probabilities"])[0, : len(labels)]
chosen = labels[int(probabilities.argmax())]
error = abs(float(probabilities.max()) - float(expected["confidence"]))
errors.append(error)
counts["checked"] += 1
if chosen != expected["label"]:
counts["mismatches"] += 1
failures.append({"suite": row["suite"], "index": row["index"], "native": expected, "coreml": chosen})
if counts["checked"] >= args.limit:
break
result = {
"model": MODEL_ID, "revision": MODEL_REVISION, "package": args.package,
"selected_manifest": "first eligible rows in source suite order; no gold labels used",
"counts": counts, "maximum_confidence_error": max(errors, default=None),
"mean_confidence_error": statistics.mean(errors) if errors else None,
"model_call_p50_ms": statistics.median(latencies[1:]) if len(latencies) > 1 else None,
"model_call_p95_ms": sorted(latencies[1:])[int(0.95 * (len(latencies) - 1))] if len(latencies) > 1 else None,
"failures": failures[:20],
}
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
Path(args.out).write_text(json.dumps(result, indent=2) + "\n")
print(json.dumps(result, indent=2))
if counts["checked"] == 0 or counts["mismatches"]:
raise SystemExit(1)
if __name__ == "__main__":
main()