| """Compare a converted Kev package with the merged PyTorch checkpoint.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import statistics |
| import time |
| from pathlib import Path |
|
|
| import coremltools as ct |
| import numpy as np |
| import torch |
|
|
| from assets import load_model |
| from export_model import KevExport |
| from preprocessing import Shape, prepare_inputs |
|
|
| COMPUTE_UNITS = { |
| "all": ct.ComputeUnit.ALL, |
| "cpu": ct.ComputeUnit.CPU_ONLY, |
| "cpu-gpu": ct.ComputeUnit.CPU_AND_GPU, |
| "cpu-ne": ct.ComputeUnit.CPU_AND_NE, |
| } |
|
|
|
|
| def fixtures() -> list[dict]: |
| return [ |
| { |
| "state": "The piece leaves one hole beneath it and creates a small bump on top.", |
| "questions": { |
| "q": { |
| "type": "choice", |
| "instructions": "Classify the placement.", |
| "criteria": { |
| "clean": "No buried holes and a flat surface", |
| "risky": "Creates a cavity or awkward surface", |
| }, |
| "label": "risky", |
| "src": "coreml-fixture", |
| } |
| }, |
| }, |
| { |
| "state": "URGENT: verify your account at http://unknown.example and enter your password.", |
| "questions": { |
| "q": { |
| "type": "noul", |
| "instructions": "Is this message phishing?", |
| "criteria": {"false": "legitimate", "true": "phishing"}, |
| "label": True, |
| "src": "coreml-fixture", |
| } |
| }, |
| }, |
| { |
| "state": "The customer was charged twice and wants the duplicate transaction reversed.", |
| "questions": { |
| "q": { |
| "type": "choice", |
| "instructions": "Route this support ticket.", |
| "criteria": { |
| "billing": "Payments and charges", |
| "technical": "Product malfunction", |
| "sales": "Buying a product", |
| }, |
| "label": "billing", |
| "src": "coreml-fixture", |
| } |
| }, |
| }, |
| { |
| "state": "The order arrived two weeks late and the outer box was damaged.", |
| "questions": { |
| "q": { |
| "type": "score", |
| "instructions": "Rate the delivery issue severity.", |
| "criteria": ["Low impact", "Moderate impact", "High impact"], |
| "label": 2, |
| "src": "coreml-fixture", |
| } |
| }, |
| }, |
| ] |
|
|
|
|
| def suite_requests(path: Path, limit: int) -> list[dict]: |
| requests = [] |
| per_suite: dict[str, int] = {} |
| for line in path.read_text().splitlines(): |
| row = json.loads(line) |
| if per_suite.get(row["suite"], 0) >= 2: |
| continue |
| if row["type"] == "choice": |
| question = { |
| "type": "choice", |
| "instructions": row["instructions"], |
| "criteria": {key: description for key, description in row["options"]}, |
| "label": row["options"][row["gold"]][0], |
| } |
| else: |
| question = { |
| "type": "noul", |
| "instructions": row["instructions"], |
| "criteria": ( |
| {key: description for key, description in row["options"]} if row["options"] else None |
| ), |
| "label": bool(row["gold"]), |
| } |
| question["src"] = row["suite"] |
| requests.append({"state": json.loads(row["state"]), "questions": {"q": question}}) |
| per_suite[row["suite"]] = per_suite.get(row["suite"], 0) + 1 |
| if len(requests) == limit: |
| break |
| return requests |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument("package", type=Path) |
| parser.add_argument("--length", type=int, default=128) |
| parser.add_argument("--max-options", type=int, default=32) |
| parser.add_argument("--units", choices=COMPUTE_UNITS, default="all") |
| parser.add_argument("--max-probability-error", type=float, default=0.02) |
| parser.add_argument("--suite", type=Path) |
| parser.add_argument("--suite-cases", type=int, default=20) |
| args = parser.parse_args() |
| _, tokenizer, decision_model = load_model() |
| decision_model.eval() |
| shape = Shape(args.length, args.max_options) |
| wrapper = KevExport(decision_model, shape.length, shape.max_options).eval() |
| started = time.perf_counter() |
| coreml = ct.models.MLModel(str(args.package), compute_units=COMPUTE_UNITS[args.units]) |
| load_seconds = time.perf_counter() - started |
| errors: list[float] = [] |
| times: list[float] = [] |
| agreements = 0 |
| evaluated = 0 |
| requests = fixtures() |
| if args.suite: |
| requests.extend(suite_requests(args.suite, args.suite_cases)) |
| for index, request in enumerate(requests): |
| try: |
| arrays, encoded = prepare_inputs(decision_model, tokenizer, request, shape) |
| except ValueError as error: |
| print(f"fixture {index}: skipped ({error})") |
| continue |
| evaluated += 1 |
| tensors = tuple(torch.from_numpy(value) for value in arrays.values()) |
| with torch.no_grad(): |
| _, reference = wrapper(*tensors) |
| started = time.perf_counter() |
| output = coreml.predict(arrays) |
| times.append((time.perf_counter() - started) * 1000) |
| options = len(encoded["opt_idx"][0]) |
| expected = reference[0, :options].numpy() |
| actual = np.asarray(output["probabilities"])[0, :options] |
| error = float(np.max(np.abs(expected - actual))) |
| errors.append(error) |
| agreement = int(expected.argmax()) == int(actual.argmax()) |
| agreements += agreement |
| print(f"fixture {index}: options={options} argmax={agreement} max_probability_error={error:.6f}") |
| p95 = sorted(times)[max(0, int(0.95 * len(times)) - 1)] |
| print( |
| f"{agreements}/{evaluated} argmax; max_probability_error={max(errors):.6f}; " |
| f"p50={statistics.median(times):.2f} ms; p95={p95:.2f} ms; load={load_seconds:.2f} s" |
| ) |
| if agreements != evaluated or max(errors) > args.max_probability_error: |
| raise SystemExit(1) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|