File size: 6,707 Bytes
a99edfc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""python -m rlcd.benchmark --device mps --output results/m2-max.json"""
import argparse
from datetime import datetime, timezone
import hashlib
import importlib.metadata
import json
import platform
import random
import statistics
import subprocess
import time
from pathlib import Path
import jsonschema
import numpy as np
import torch
from .engine import Engine, MODEL_ID, REVISION
from .tasks import CASES


def evaluate(text, schema, expected):
    try:
        obj = json.loads(text, parse_constant=lambda s: (_ for _ in ()).throw(ValueError(s)))
        syntax = True
    except (ValueError, TypeError):
        obj, syntax = None, False
    compliant = syntax and jsonschema.Draft202012Validator(schema).is_valid(obj)
    correct = sum(type(obj.get(k)) is type(v) and obj.get(k) == v for k, v in expected.items()) if isinstance(obj, dict) else 0
    return {"syntax_valid": syntax, "schema_compliant": compliant, "correct_fields": correct,
            "field_count": len(expected), "exact_match": compliant and correct == len(expected)}


def metadata(engine):
    info = {"recorded_at_utc": datetime.now(timezone.utc).isoformat(),
            "all_dependencies": {d.metadata["Name"]:d.version for d in importlib.metadata.distributions()},
            "platform": platform.platform(), "python": platform.python_version(),
            "versions": {k: importlib.metadata.version(k) for k in ["torch", "transformers", "accelerate", "huggingface-hub", "jsonschema", "modal"]},
            "device": engine.device, "dtype": engine.dtype, "attention": "eager", "quantization": None,
            "model": MODEL_ID, "revision": REVISION, "cuda_runtime": torch.version.cuda,
            "mps_available": torch.backends.mps.is_available(),
            "source_sha256": {str(p): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(Path(__file__).parent.glob("*.py"))}}
    if engine.device == "mps":
        info["hardware"] = subprocess.check_output(["sysctl", "-n", "machdep.cpu.brand_string"], text=True).strip()
        info["memory_bytes"] = int(subprocess.check_output(["sysctl", "-n", "hw.memsize"], text=True))
    if engine.device.startswith("cuda"):
        info["hardware"] = torch.cuda.get_device_name()
        info["gpu_info"] = subprocess.check_output(["nvidia-smi", "--query-gpu=name,driver_version,memory.total", "--format=csv,noheader"], text=True).strip()
        info["tf32_matmul"] = torch.backends.cuda.matmul.allow_tf32
    return info


def summarize(rows):
    result = {}
    for method in ["constrained", "autoregressive"]:
        selected = [r for r in rows if r["method"] == method]
        times = [r["latency_ms"] for r in selected]
        result[method] = {"n": len(times), "latency_mean_ms": statistics.mean(times),
                          "latency_median_ms": statistics.median(times), "latency_p95_ms": float(np.percentile(times, 95)),
                          "syntax_valid_rate": statistics.mean(r["syntax_valid"] for r in selected),
                          "schema_compliant_rate": statistics.mean(r["schema_compliant"] for r in selected),
                          "field_accuracy": sum(r["correct_fields"] for r in selected) / sum(r["field_count"] for r in selected),
                          "exact_match_rate": statistics.mean(r["exact_match"] for r in selected)}
    result["speedup_ratio_of_mean_latency"] = result["autoregressive"]["latency_mean_ms"] / result["constrained"]["latency_mean_ms"]
    return result


def run(device="mps", dtype="float16", warmups=2, repeats=3, limit=None, suite="diagnostic", max_new_tokens=192):
    torch.manual_seed(42)
    torch.set_num_threads(4)
    engine = Engine(device, dtype)
    if warmups < 1 or repeats < 2:
        raise ValueError("Use at least one warmup and two measured repetitions")
    from .stress_tasks import CASES as STRESS_CASES
    all_cases = CASES if suite == "diagnostic" else STRESS_CASES
    cases = all_cases[:limit] if limit else all_cases
    def invoke(method, context, schema):
        kwargs = {"max_new_tokens": max_new_tokens} if method == "autoregressive" else {}
        return getattr(engine, method)(context, schema, **kwargs)
    # Warm every case and both methods, then discard timings.
    for _ in range(warmups):
        for _, schema, context, _ in cases:
            for method in ["constrained", "autoregressive"]:
                invoke(method, context, schema)
        engine.sync()
    rows = []
    for rep in range(repeats):
        order = list(cases)
        random.Random(42 + rep).shuffle(order)
        for case_id, schema, context, expected in order:
            methods = ["constrained", "autoregressive"]
            if (rep + all_cases.index(next(c for c in all_cases if c[0] == case_id))) % 2:
                methods.reverse()
            for method in methods:
                engine.sync()
                started = time.perf_counter()
                output = invoke(method, context, schema)
                engine.sync()
                latency = (time.perf_counter() - started) * 1000
                row = {"case": case_id, "repeat": rep, "method": method, "latency_ms": latency,
                       **output, **evaluate(output["text"], schema, expected)}
                rows.append(row)
                print(f'{device} {rep} {case_id} {method}: {latency:.1f}ms exact={row["exact_match"]}', flush=True)
    return {"metadata": metadata(engine), "protocol": {"warmups_per_case_per_method": warmups, "repeats": repeats,
            "seed": 42, "max_new_tokens": max_new_tokens, "suite": suite, "timing": "synchronized end-to-end request; includes tokenization, cache copy, model calls, scoring, decode/assembly; excludes model load and validation",
            "cases": len(cases), "order": "seeded shuffled cases; alternating paired method order", "batch_size_requests": 1},
            "summary": summarize(rows), "rows": rows}

if __name__ == "__main__":
    p = argparse.ArgumentParser()
    p.add_argument("--device", default="mps")
    p.add_argument("--dtype", default="float16", choices=["float16", "bfloat16", "float32"])
    p.add_argument("--warmups", type=int, default=2)
    p.add_argument("--repeats", type=int, default=3)
    p.add_argument("--limit", type=int)
    p.add_argument("--suite", choices=["diagnostic", "stress"], default="diagnostic")
    p.add_argument("--max-new-tokens", type=int, default=192)
    p.add_argument("--output", default="results/m2-max.json")
    args = vars(p.parse_args())
    output = Path(args.pop("output"))
    result = run(**args)
    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(json.dumps(result, indent=2) + "\n")
    print(json.dumps(result["summary"], indent=2))