File size: 5,795 Bytes
55bd028
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""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("--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:
        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")
    print(json.dumps({key: value for key, value in report.items() if key != "cases"}, 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()