"""Device benchmark: latency vs temperature under sustained load, plus accuracy, for the numpy runtime. python bench_device.py --model ../model --data --out result.json [--threads-probe] Phases: idle temperature (30 s) -> warm-up -> every prompt of every eval set once (sustained load; stats per 60 s window: temperature, CPU clock, latency p50/p95, prompts/s) -> optional 1-thread vs default-thread probe. Accuracy = runtime output (with snap) == target, i.e. what `run.sh test --full` measures, not the raw Colab number. Temperature: Raspberry Pi = SoC sensor (vcgencmd); Windows = ACPI thermal zone (not the CPU core sensor). """ import argparse, json, os, platform, statistics, subprocess, sys, threading, time SETS = ["curated", "wifi_holdout", "gap11_holdout", "B_golden_final", "A_practical-holdout"] def is_pi(): return os.path.exists("/usr/bin/vcgencmd") PS = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "WindowsPowerShell", "v1.0", "powershell.exe") def sample(): """-> dict(temp_c, clock_mhz, throttled) for this device; missing values are None.""" if is_pi(): def v(*a): return subprocess.run(["vcgencmd", *a], capture_output=True, text=True).stdout.strip() t = float(v("measure_temp").split("=")[1].rstrip("'C")); f = int(v("measure_clock", "arm").split("=")[1]) / 1e6 fan = open("/sys/class/thermal/cooling_device0/cur_state").read().strip() if os.path.exists("/sys/class/thermal/cooling_device0/cur_state") else None return {"temp_c": t, "clock_mhz": round(f), "throttled": v("get_throttled").split("=")[1], "fan_state": fan} if os.name == "nt": ps = ("$c=Get-Counter '\\Thermal Zone Information(*)\\Temperature','\\Processor Information(_Total)\\% Processor Performance','\\Processor Information(_Total)\\Processor Frequency';" "$c.CounterSamples | ForEach-Object { $_.Path + '=' + $_.CookedValue }") out = subprocess.run([PS, "-NoProfile", "-Command", ps], capture_output=True, text=True).stdout temps, perf, freq = [], None, None for line in out.splitlines(): k, _, val = line.rpartition("=") if not val: continue if "temperature" in k.lower(): temps.append(float(val) - 273.15) elif "% processor performance" in k.lower(): perf = float(val) elif "processor frequency" in k.lower(): freq = float(val) clock = round(freq * perf / 100) if freq and perf else None return {"temp_c": round(max(temps), 1) if temps else None, "clock_mhz": clock, "throttled": None, "fan_state": None} return {"temp_c": None, "clock_mhz": None, "throttled": None, "fan_state": None} class Sampler(threading.Thread): def __init__(self, every=10): super().__init__(daemon=True); self.every, self.rows, self.stop = every, [], False def run(self): while not self.stop: s = sample(); s["t"] = time.time(); self.rows.append(s); time.sleep(self.every) def pct(xs, p): xs = sorted(xs); return xs[min(len(xs) - 1, int(round(p / 100 * (len(xs) - 1))))] def device_info(): info = {"platform": platform.platform(), "python": platform.python_version(), "machine": platform.machine()} import numpy; info["numpy"] = numpy.__version__ if is_pi(): info["model"] = open("/proc/device-tree/model").read().strip("\x00 \n"); info["cores"] = os.cpu_count() info["governor"] = open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor").read().strip() elif os.name == "nt": out = subprocess.run([PS, "-NoProfile", "-Command", "(Get-CimInstance Win32_Processor).Name"], capture_output=True, text=True).stdout.strip() info["model"] = out; info["cores"] = os.cpu_count() return info def load_sets(data): rows = [] for s in SETS: p = os.path.join(data, s + ".jsonl") if os.path.exists(p): rows += [(s, json.loads(l)) for l in open(p, encoding="utf-8")] return rows def main(): ap = argparse.ArgumentParser(); ap.add_argument("--model", required=True); ap.add_argument("--data", required=True); ap.add_argument("--out", required=True) ap.add_argument("--idle", type=int, default=30); ap.add_argument("--window", type=int, default=60) ap.add_argument("--probe-only", action="store_true"); ap.add_argument("--probe-n", type=int, default=200) ap.add_argument("--minutes", type=float, default=10, help="sustained-load duration (one full pass of all sets at minimum)") a = ap.parse_args() sys.path.insert(0, os.path.abspath(a.model)); from runtime import CommandModel t0 = time.time(); m = CommandModel(os.path.abspath(a.model)); load_s = time.time() - t0 rows = load_sets(a.data) import random; random.Random(0).shuffle(rows) # same mixed order everywhere, so every window/pass has the same workload if a.probe_only: # one process per thread setting: env vars must be set before numpy loads lat = [] for _, r in rows[:20]: m.predict_raw(r["text"]) for _, r in rows[:a.probe_n]: t = time.perf_counter(); m.predict_raw(r["text"]); lat.append((time.perf_counter() - t) * 1000) json.dump({"n": len(lat), "p50_ms": round(pct(lat, 50), 1), "p95_ms": round(pct(lat, 95), 1), "mean_ms": round(statistics.mean(lat), 1)}, open(a.out, "w")); return res = {"device": device_info(), "model_load_s": round(load_s, 2), "params": int(sum(v.size for v in m.W.values()))} print(res["device"], "load", res["model_load_s"], "s", flush=True) idle = [sample() for _ in range(max(1, a.idle // 5)) if not time.sleep(5)] res["idle"] = {"temp_c": round(statistics.mean(s["temp_c"] for s in idle), 1) if idle[0]["temp_c"] is not None else None, "clock_mhz": idle[-1]["clock_mhz"]} print("idle", res["idle"], flush=True) for _, r in rows[:20]: m.predict_raw(r["text"]) # warm-up smp = Sampler(10); smp.start(); t_start = time.time(); recs = []; i = 0 while i < len(rows) or time.time() - t_start < a.minutes * 60: # at least one full pass, then loop until --minutes s, r = rows[i % len(rows)] t = time.perf_counter(); pred = m.predict_raw(r["text"])[0]; ms = (time.perf_counter() - t) * 1000 recs.append({"set": s, "ok": pred == r["target"], "act_ok": pred.split()[0] == r["action"], "ms": ms, "t": time.time(), "first": i < len(rows), "pass": i // len(rows) + 1}) if i % 500 == 0: print(f" {i} {ms:.0f} ms {smp.rows[-1]['temp_c'] if smp.rows else ''} C {time.time()-t_start:.0f}s", flush=True) i += 1 smp.stop = True; wall = time.time() - t_start windows = [] for w in range(int(wall // a.window) + 1): lo, hi = t_start + w * a.window, t_start + (w + 1) * a.window rs = [x for x in recs if lo <= x["t"] < hi]; ss = [x for x in smp.rows if lo <= x["t"] < hi] if len(rs) < 5: continue temps = [x["temp_c"] for x in ss if x["temp_c"] is not None]; clk = [x["clock_mhz"] for x in ss if x["clock_mhz"]] windows.append({"minute": w + 1, "n": len(rs), "temp_c": round(max(temps), 1) if temps else None, "clock_mhz": round(statistics.mean(clk)) if clk else None, "mean_ms": round(statistics.mean(x["ms"] for x in rs), 1), "p50_ms": round(pct([x["ms"] for x in rs], 50), 1), "p95_ms": round(pct([x["ms"] for x in rs], 95), 1), "prompts_per_s": round(len(rs) / min(a.window, hi - max(lo, recs[0]["t"])), 2), "throttled": sorted({x["throttled"] for x in ss if x["throttled"]}), "fan_state": sorted({x["fan_state"] for x in ss if x["fan_state"]})}) lat = [x["ms"] for x in recs] res["load"] = {"prompts": len(recs), "wall_s": round(wall, 1), "p50_ms": round(pct(lat, 50), 1), "p95_ms": round(pct(lat, 95), 1), "p99_ms": round(pct(lat, 99), 1), "mean_ms": round(statistics.mean(lat), 1), "max_temp_c": max((w["temp_c"] for w in windows if w["temp_c"] is not None), default=None), "throttled_any": sorted({f for x in smp.rows if x["throttled"] for f in [x["throttled"]]})} res["windows"] = windows passes = [] # identical workload per pass: the clean comparison of speed as the device heats up for pn in sorted({x["pass"] for x in recs}): rs = [x for x in recs if x["pass"] == pn]; lo, hi = rs[0]["t"], rs[-1]["t"] ss = [x for x in smp.rows if lo - 10 <= x["t"] <= hi]; temps = [x["temp_c"] for x in ss if x["temp_c"] is not None] clk = [x["clock_mhz"] for x in ss if x["clock_mhz"]]; ms = [x["ms"] for x in rs] passes.append({"pass": pn, "n": len(rs), "complete": len(rs) == len(rows), "temp_min_c": min(temps) if temps else None, "temp_max_c": max(temps) if temps else None, "clock_mhz": round(statistics.mean(clk)) if clk else None, "mean_ms": round(statistics.mean(ms), 1), "p50_ms": round(pct(ms, 50), 1), "p95_ms": round(pct(ms, 95), 1), "prompts_per_s": round(len(rs) / max(hi - lo, 1e-9), 2)}) res["passes"] = passes first =[x for x in recs if x["first"]] res["accuracy"] = {s: {"n": sum(1 for x in first if x["set"] == s), "exact": round(100 * sum(x["ok"] for x in first if x["set"] == s) / max(1, sum(1 for x in first if x["set"] == s)), 2), "action": round(100 * sum(x["act_ok"] for x in first if x["set"] == s) / max(1, sum(1 for x in first if x["set"] == s)), 2)} for s in SETS} res["accuracy_all"] = {"n": len(first), "exact": round(100 * sum(x["ok"] for x in first) / len(first), 2), "action": round(100 * sum(x["act_ok"] for x in first) / len(first), 2)} res["threads"] = {k: os.environ.get(k) for k in ("OPENBLAS_NUM_THREADS", "OMP_NUM_THREADS")} json.dump(res, open(a.out, "w"), indent=1); print(json.dumps(res["load"]), flush=True) if __name__ == "__main__": main()