| """Measure Verdict LUT8 against FP16 on fixed Decision Index requests without using gold labels.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import gzip |
| import json |
| import platform |
| import statistics |
| import subprocess |
| import time |
| from collections import Counter |
| from pathlib import Path |
|
|
| import coremltools as ct |
| import numpy as np |
| from transformers import AutoTokenizer |
|
|
| from assets import ROOT, sha256, verify_assets |
| from decision_index_engine import adapt_question, as_text |
| from native_reference import build_request, decode, load_calibrator |
| from preprocessing import prepare |
|
|
|
|
| def selected_requests(rows_path: Path, tokenizer, limit: int, length: int): |
| """First ten eligible rows per family, before inspecting either model's output.""" |
| family_counts = Counter() |
| skipped = Counter() |
| selected = [] |
| with gzip.open(rows_path, "rt") as stream: |
| for line in stream: |
| row = json.loads(line) |
| family = row["family"] |
| if family_counts[family] >= 10: |
| continue |
| if len(row["questions"]) != 1: |
| skipped["multiple_questions"] += 1 |
| continue |
| question = next(iter(row["questions"].values())) |
| if question["type"] not in ("choice", "noul"): |
| skipped["unsupported_question_type"] += 1 |
| continue |
| try: |
| request = build_request(as_text(row["state"]), adapt_question(question)) |
| except ValueError: |
| skipped["invalid_or_overcapacity"] += 1 |
| continue |
| tokens = len(tokenizer(request.text, truncation=False)["input_ids"]) |
| if tokens > length: |
| skipped["overlength"] += 1 |
| continue |
| selected.append((row["id"], family, request, tokens)) |
| family_counts[family] += 1 |
| if len(selected) >= limit: |
| break |
| return selected, family_counts, skipped |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument("--rows", type=Path, required=True, help="pinned public Decision Index selected-rows.jsonl.gz") |
| parser.add_argument("--limit", type=int, default=100) |
| parser.add_argument("--length", type=int, default=128) |
| parser.add_argument("--repeats", type=int, default=5) |
| args = parser.parse_args() |
| if args.length != 128: |
| raise ValueError("the predeclared LUT8 validation protocol is L128 only") |
| if args.limit != 100: |
| raise ValueError("the predeclared validation manifest uses exactly 100 requests") |
| source = verify_assets(required=("config.json", "tokenizer.json", "tokenizer_config.json", "calibrator.json")) |
| tokenizer = AutoTokenizer.from_pretrained(source) |
| config = json.loads((source / "config.json").read_text()) |
| calibrator = load_calibrator(source) |
| selected, families, skipped = selected_requests(args.rows, tokenizer, args.limit, args.length) |
| if len(selected) != args.limit: |
| raise ValueError(f"only {len(selected)} eligible fixed requests; expected {args.limit}") |
|
|
| packages = { |
| "fp16": ROOT / "build" / "verdict_fp16_L128_candidates25.mlpackage", |
| "lut8": ROOT / "build" / "verdict_lut8_kmeans_per_tensor_L128_candidates25.mlpackage", |
| } |
| models = {name: ct.models.MLModel(str(path), compute_units=ct.ComputeUnit.ALL) for name, path in packages.items()} |
| for _, _, request, _ in selected[:2]: |
| arrays = prepare(tokenizer, config["class_token_index"], request.text, args.length, 25) |
| for model in models.values(): |
| model.predict(arrays) |
|
|
| rows = [] |
| timings = {name: [] for name in models} |
| for index, (row_id, family, request, tokens) in enumerate(selected): |
| arrays = prepare(tokenizer, config["class_token_index"], request.text, args.length, 25) |
| results = {} |
| for name, model in models.items(): |
| output = model.predict(arrays) |
| results[name] = decode(output["logits"], request, calibrator) |
| if index < 20: |
| for _ in range(args.repeats): |
| start = time.perf_counter() |
| model.predict(arrays) |
| timings[name].append((time.perf_counter() - start) * 1000) |
| reference = results["fp16"] |
| compressed = results["lut8"] |
| differences = [abs(reference["probabilities"][key] - compressed["probabilities"][key]) for key in request.ids] |
| rows.append( |
| { |
| "id": row_id, |
| "family": family, |
| "tokens": tokens, |
| "candidates": len(request.ids), |
| "fp16_selected_id": reference["selected_id"], |
| "lut8_selected_id": compressed["selected_id"], |
| "selection_agrees": reference["selected_id"] == compressed["selected_id"], |
| "abstention_agrees": reference["is_abstention"] == compressed["is_abstention"], |
| "max_probability_error": max(differences), |
| } |
| ) |
| errors = np.array([row["max_probability_error"] for row in rows]) |
| selection_agreement = sum(row["selection_agrees"] for row in rows) / len(rows) |
| abstention_agreement = sum(row["abstention_agrees"] for row in rows) / len(rows) |
| gates = { |
| "min_selection_agreement": 0.99, |
| "min_abstention_agreement": 0.99, |
| "max_p95_probability_error": 0.02, |
| "max_worst_probability_error": 0.10, |
| } |
| report = { |
| "suite_file": args.rows.name, |
| "suite_sha256": sha256(args.rows), |
| "selection_protocol": "First 10 eligible rows per family in suite order, 100 total; no gold labels used", |
| "selected_row_ids": [row["id"] for row in rows], |
| "families": dict(families), |
| "skipped_before_limit": dict(skipped), |
| "packages": {name: path.name for name, path in packages.items()}, |
| "hardware": { |
| "chip": subprocess.run( |
| ["sysctl", "-n", "machdep.cpu.brand_string"], capture_output=True, text=True |
| ).stdout.strip(), |
| "macos": platform.mac_ver()[0], |
| }, |
| "questions": len(rows), |
| "selection_agreement": selection_agreement, |
| "abstention_agreement": abstention_agreement, |
| "p95_probability_error": float(np.percentile(errors, 95)), |
| "worst_probability_error": float(errors.max()), |
| "median_model_call_ms": {name: statistics.median(values) for name, values in timings.items()}, |
| "gates": gates, |
| "rows": rows, |
| } |
| report["passed"] = ( |
| selection_agreement >= gates["min_selection_agreement"] |
| and abstention_agreement >= gates["min_abstention_agreement"] |
| and report["p95_probability_error"] <= gates["max_p95_probability_error"] |
| and report["worst_probability_error"] <= gates["max_worst_probability_error"] |
| ) |
| target = ROOT / "reports" / "lut8-L128-suite-parity.json" |
| target.write_text(json.dumps(report, indent=2) + "\n") |
| print( |
| json.dumps({key: value for key, value in report.items() if key not in ("rows", "selected_row_ids")}, indent=2) |
| ) |
| if not report["passed"]: |
| raise SystemExit(1) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|