File size: 5,263 Bytes
61b6fb9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Accuracy and load test for serve_jev.py (stdlib only).

    python scripts/bench_jev.py accuracy URL CASES.jsonl                 # same cases as the offline test
    python scripts/bench_jev.py load URL [--cases CASES.jsonl] [--levels 1,8,32,128,256] [--n 2000] [--procs 8]

load: closed loop, `c` clients spread over --procs generator processes (one Python process is one
core, it would measure itself). Without --cases a request is the small 2-question one used for
the other Jev servers (choice of 3 + noul); with --cases, each request is one real case with all
its questions.
"""
import argparse
import collections
import json
import multiprocessing as mp
import sys
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor

SMALL = {"model": "jev-latest", "state": "Me habéis cobrado dos veces la cuota de septiembre, devolvedme una ya.",
         "questions": {"dep": {"type": "choice", "instructions": "¿Qué departamento?",
                               "criteria": {"administracion": "recibos", "mantenimiento": "averías", "juridico": "morosos"}},
                       "urg": {"type": "noul", "instructions": "¿Es urgente?"}}}


def post(url, body, timeout=300):
    req = urllib.request.Request(url, json.dumps(body).encode(), {"Content-Type": "application/json"})
    t = time.perf_counter()
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            return r.status, json.loads(r.read()), time.perf_counter() - t
    except urllib.error.HTTPError as e:
        return e.code, None, time.perf_counter() - t
    except Exception as e:  # noqa: BLE001
        return type(e).__name__, None, time.perf_counter() - t


def body_of(c):
    return {"state": c["state"], "questions": c["questions"], "lang": c.get("lang", "es")}


def accuracy(url, path):
    cases = [json.loads(x) for x in open(path, encoding="utf-8") if x.strip()]
    ok = tot = 0
    with ThreadPoolExecutor(32) as ex:
        for c, (st, out, _) in zip(cases, ex.map(lambda c: post(url, body_of(c)), cases)):
            for qid, q in c["questions"].items():
                a = out["answers"][qid]
                if q["type"] == "noul":
                    pred = "true" if a["noul"] >= 0.5 else "false"
                else:
                    pred = max(a["probabilities"], key=a["probabilities"].get)
                ok += pred in c["gold"][qid]
                tot += 1
    print("accuracy %d/%d (%.1f %%)" % (ok, tot, 100 * ok / tot))


def _worker(args):
    url, bodies, n, threads, warm = args
    def one(i):
        st, out, dt = post(url, bodies[i % len(bodies)])
        return (st, dt, (sum(1 for _ in out["answers"]) if out else 0),
                ((out.get("usage") or {}).get("input_tokens", 0) if out else 0))
    with ThreadPoolExecutor(threads) as ex:
        list(ex.map(one, range(min(warm, n))))
        return list(ex.map(one, range(n)))


def load(url, bodies, levels, n, procs):
    health = url.rsplit("/v1/", 1)[0] + "/health"
    for c in levels:
        p = min(procs, c)
        per = [c // p + (i < c % p) for i in range(p)]
        reqs = [max(1, n * k // c) for k in per]
        def stats():
            try:
                with urllib.request.urlopen(health, timeout=10) as r:
                    return json.loads(r.read())["batching"]
            except Exception:  # noqa: BLE001
                return {}
        s0 = stats()
        t0 = time.time()
        with mp.Pool(p) as pool:
            res = [x for part in pool.map(_worker, [(url, bodies[i::p] or bodies, reqs[i], per[i], per[i]) for i in range(p)])
                   for x in part]
        dur = time.time() - t0
        s1 = stats()
        ms = sorted(x[1] * 1000 for x in res)
        q = sum(x[2] for x in res)
        tok_q = sum(x[3] for x in res) / max(1, q)
        k = len(ms)
        rows = (s1.get("rows", 0) - s0.get("rows", 0)) / max(1, s1.get("calls", 0) - s0.get("calls", 0))
        busy = (s1.get("gpu_s", s1.get("busy_s", 0)) - s0.get("gpu_s", s0.get("busy_s", 0))) / dur
        print("  c=%-4d n=%-5d %7.0f req/s %7.0f questions/s   p50 %6.1f ms  p90 %6.1f  p95 %6.1f  p99 %6.1f   "
              "input tok/question %6.1f   rows/forward %5.1f  in forward %3.0f %%  %s"
              % (c, k, k / dur, q / dur, ms[k // 2], ms[int(k * .9)], ms[int(k * .95)], ms[min(k - 1, int(k * .99))],
                 tok_q, rows, 100 * busy, dict(collections.Counter(x[0] for x in res))), flush=True)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("mode", choices=["accuracy", "load"])
    ap.add_argument("url")
    ap.add_argument("cases", nargs="?")
    ap.add_argument("--cases", dest="cases_opt")
    ap.add_argument("--levels", default="1,8,32,128,256")
    ap.add_argument("--n", type=int, default=2000)
    ap.add_argument("--procs", type=int, default=8)
    a = ap.parse_args()
    url = a.url.rstrip("/") + "/v1/systemone"
    if a.mode == "accuracy":
        return accuracy(url, a.cases)
    src = a.cases_opt or a.cases
    bodies = [body_of(json.loads(x)) for x in open(src, encoding="utf-8") if x.strip()] if src else [SMALL]
    load(url, bodies, [int(x) for x in a.levels.split(",")], a.n, a.procs)


if __name__ == "__main__":
    sys.exit(main())