File size: 9,159 Bytes
df44977
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""The source model card's examples on this repository's LiteRT files, through the PyTorch-free host.

    python examples/run_example.py                         # CPU (XNNPACK, 8 threads), the single files
    python examples/run_example.py --accel gpu             # the GPU at float32 (Metal on a Mac)
    python examples/run_example.py --check                 # compare with examples/run_example.expected.json
    python examples/run_example.py --form multi8 --accel gpu --check   # the eight-signature file instead

The examples of the LiquidAI/d1-2B model card: a support ticket with four questions (yes/no, a pick, several picks, a
rating), values from an invoice (text, number, date, and a number that is absent), the card's photo with a count, one
box and every cat, and two tickets in one call. Then a state too long for the largest graph, which the host refuses.
The photo (COCO val2017 000000039769.jpg, 640 x 480) is not in this repository: the script downloads it into memory and
checks its SHA-256 first (--image reads a local copy instead).

--check compares every answer with run_example.expected.json, the answers of the provider's own code in float32 on the
CPU: probabilities, P(found) and P(yes) within 1e-4, box edges within 0.002 (two of the model's 0.001 steps), box
confidence within 0.02, and everything else equal (the picked options, texts, spans, numbers, dates, counts and the
token counts); and the refusal. It prints CHECK PASS or CHECK FAIL and exits 1 on a failure.
"""
from __future__ import annotations

import argparse
import hashlib
import io
import json
import math
import os
import sys
import time
import urllib.request

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT, "host"))

PHOTO_URL = "http://images.cocodataset.org/val2017/000000039769.jpg"
PHOTO_SHA256 = "dea9e7ef97386345f7cff32f9055da4982da5471c48d575146c796ab4563b04e"

QUESTIONS = {
    "refund": {"type": "noul", "instructions": "Is the customer asking for a refund?"},
    "team": {"type": "choice", "instructions": "Which team should handle this?",
             "criteria": {"billing": "Charges, refunds, invoices", "shipping": "Delivery and parcels",
                          "technical": "App or site faults"}},
    "topics": {"type": "choice", "multiple": True, "instructions": "Which topics does the message raise?",
               "criteria": {"billing": "Payments and charges", "delivery": "Shipping and parcels",
                            "login": "Signing in to the account"}},
    "urgency": {"type": "score", "instructions": "How urgent is this ticket?", "criteria": ["low", "medium", "high"]},
}
TICKET = "I was charged twice and my parcel never arrived. Please refund one charge."
INVOICE = "ACME SA\nInvoice 2026-117\nTotal due: 1 234,50 EUR\nPay before 03/04/2026 (day/month/year)"
INVOICE_QUESTIONS = {
    "payee": {"type": "text", "instructions": "Who issued the invoice?"},
    "total": {"type": "number", "instructions": "How much is due?"},
    "due": {"type": "date", "instructions": "By when must it be paid?"},
    "vat": {"type": "number", "instructions": "How much VAT is charged?"},
}
PHOTO_QUESTIONS = {
    "cats": {"type": "number", "instructions": "How many cats are there?", "range": [0, 10], "step": 1},
    "remote": {"type": "box", "instructions": "Where is the remote control on the left?"},
    "every_cat": {"type": "boxes", "instructions": "Every cat.", "max": 4},
}
TICKETS = ["Where is my parcel? It was due Monday.", "The app crashes when I open settings."]
LONG_STATE = "The parcel left the depot on time and reached the sorting centre a day later. " * 600

EDGES = ("top", "bottom", "left", "right")


def photo(path: str | None):
    from PIL import Image

    data = open(path, "rb").read() if path else urllib.request.urlopen(PHOTO_URL, timeout=60).read()
    digest = hashlib.sha256(data).hexdigest()
    if digest != PHOTO_SHA256:
        raise SystemExit(f"the photo's SHA-256 is {digest}, not {PHOTO_SHA256}")
    return Image.open(io.BytesIO(data))


def compare(ref, got, path, out, qtype=""):
    """Append to `out` every difference outside the tolerances of the module docstring. `qtype` is the answer's type:
    the box confidence (a fraction of 512 sample boxes) is compared within 0.02, a choice's confidence within 1e-4."""
    key = path.rsplit(".", 1)[-1]
    if isinstance(ref, dict) and isinstance(got, dict):
        qtype = ref.get("type", qtype) if isinstance(ref.get("type"), str) else qtype
        if set(ref) != set(got):
            out.append(f"{path}: keys {sorted(got)} != {sorted(ref)}")
        for k in ref:
            if k in got:
                compare(ref[k], got[k], f"{path}.{k}", out, qtype)
    elif isinstance(ref, list) and isinstance(got, list):
        if len(ref) != len(got):
            out.append(f"{path}: {len(got)} items != {len(ref)}")
        for i, (r, g) in enumerate(zip(ref, got)):
            compare(r, g, f"{path}[{i}]", out, qtype)
    elif isinstance(ref, float) and key not in ("number", "score") and ".interval" not in path:
        tol = 0.002 if key in EDGES else 0.02 if key == "confidence" and qtype in ("box", "boxes") else 1e-4
        if isinstance(got, bool) or not isinstance(got, (int, float)) or not (math.isfinite(got) and abs(got - ref) <= tol):
            out.append(f"{path}: {got!r} vs {ref!r} (tolerance {tol})")
    elif ref != got:
        out.append(f"{path}: {got!r} != {ref!r}")


def main():
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    ap.add_argument("--accel", choices=("cpu", "gpu"), default="cpu")
    ap.add_argument("--form", choices=("files", "multi8"), default="files")
    ap.add_argument("--threads", type=int, default=8)
    ap.add_argument("--weight-cache", default="", help="the XNNPACK weight cache file of --form multi8 on the CPU")
    ap.add_argument("--image", default="", help="a local copy of the card's photo")
    ap.add_argument("--check", action="store_true")
    ap.add_argument("--out", default="", help="write the answers as JSON")
    a = ap.parse_args()
    from d1_litert import load_host

    t0 = time.time()
    results, refusal = {}, None
    with load_host(ROOT, accelerator=a.accel, form=a.form, threads=a.threads,
                   weight_cache=a.weight_cache or None) as host:
        for name, call in (("ticket", lambda: host.system_one(TICKET, QUESTIONS)),
                           ("invoice", lambda: host.system_one(INVOICE, INVOICE_QUESTIONS)),
                           ("photo", lambda: host.system_one(None, PHOTO_QUESTIONS, media=[photo(a.image or None)])),
                           ("tickets", lambda: host.system_one_batch([(t, {"team": QUESTIONS["team"]})
                                                                       for t in TICKETS]))):
            t = time.time()
            results[name] = call()
            print(f"{name}: {time.time() - t:.2f} s (compiles included)")
        try:
            host.system_one(LONG_STATE, {"refund": QUESTIONS["refund"]})
            refusal = {"refused": False}
        except ValueError as e:
            refusal = {"refused": True, "type": "ValueError", "message": str(e)}
    wall = time.time() - t0

    a_t = results["ticket"]["answers"]
    print("ticket:", round(a_t["refund"]["noul"], 2), a_t["team"]["choice"], a_t["topics"]["choices"],
          a_t["urgency"]["legend"][str(int(a_t["urgency"]["score"]))])
    a_i = results["invoice"]["answers"]
    print("invoice:", a_i["payee"]["text"], a_i["total"]["number"], a_i["due"]["date"], a_i["vat"]["number"])
    a_p = results["photo"]["answers"]
    print("photo:", a_p["cats"]["number"], a_p["every_cat"]["count"],
          {k: round(v, 2) for k, v in a_p["remote"]["box"].items()})
    print("tickets:", [r["answers"]["team"]["choice"] for r in results["tickets"]])
    print("refusal:", refusal.get("message", refusal))
    print(f"wall {wall:.1f} s, accel {a.accel}, form {a.form}")
    if a.out:
        with open(a.out, "w") as f:
            json.dump({"accel": a.accel, "form": a.form, "results": results, "refusal": refusal}, f, indent=1)
    if a.check:
        exp = json.load(open(os.path.join(ROOT, "examples", "run_example.expected.json")))
        diffs = []
        for name in ("ticket", "invoice", "photo"):
            compare(exp["examples"][name]["answers"], results[name]["answers"], name, diffs)
            compare(exp["examples"][name]["usage"], results[name]["usage"], f"{name}.usage", diffs)
        for i, r in enumerate(results["tickets"]):
            compare(exp["examples"]["tickets"][i]["answers"], r["answers"], f"tickets[{i}]", diffs)
            compare(exp["examples"]["tickets"][i]["usage"], r["usage"], f"tickets[{i}].usage", diffs)
        if not refusal.get("refused") or exp["refusal"]["contains"] not in refusal.get("message", ""):
            diffs.append(f"refusal: {refusal}")
        print("\n".join(diffs) if diffs else "", end="\n" if diffs else "")
        print("CHECK PASS" if not diffs else f"CHECK FAIL ({len(diffs)} differences)")
        sys.exit(1 if diffs else 0)


if __name__ == "__main__":
    main()