tuieval 0.2.0.dev56__py3-none-any.whl → 0.2.0.dev65__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tuieval/_version.py +2 -2
- tuieval/cli.py +1 -1
- tuieval/compare.py +22 -6
- tuieval/doctor.py +17 -0
- tuieval/engine.py +39 -10
- tuieval/machines.py +163 -5
- tuieval/pta.py +71 -36
- tuieval/run_evals.py +4 -3
- tuieval/tui.py +31 -114
- tuieval/tune.py +9 -0
- tuieval/verdict.py +2 -1
- {tuieval-0.2.0.dev56.dist-info → tuieval-0.2.0.dev65.dist-info}/METADATA +5 -5
- {tuieval-0.2.0.dev56.dist-info → tuieval-0.2.0.dev65.dist-info}/RECORD +16 -16
- {tuieval-0.2.0.dev56.dist-info → tuieval-0.2.0.dev65.dist-info}/WHEEL +0 -0
- {tuieval-0.2.0.dev56.dist-info → tuieval-0.2.0.dev65.dist-info}/entry_points.txt +0 -0
- {tuieval-0.2.0.dev56.dist-info → tuieval-0.2.0.dev65.dist-info}/licenses/LICENSE +0 -0
tuieval/_version.py
CHANGED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2, 0, '
|
|
21
|
+
__version__ = version = '0.2.0.dev65'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev65')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
tuieval/cli.py
CHANGED
|
@@ -14,7 +14,7 @@ Start here
|
|
|
14
14
|
|
|
15
15
|
Day to day
|
|
16
16
|
tuieval compare [...] scorecards (--speed, --pairwise, --failures)
|
|
17
|
-
tuieval pta the PTA index: parsimony,
|
|
17
|
+
tuieval pta the PTA index: parsimony (tokens), time and accuracy per model, as bars
|
|
18
18
|
tuieval report the verdicts with their evidence, as a markdown file
|
|
19
19
|
tuieval history every verdict change, newest first
|
|
20
20
|
tuieval items tests that don't separate models or look broken
|
tuieval/compare.py
CHANGED
|
@@ -35,7 +35,8 @@ def _load_native(data):
|
|
|
35
35
|
m, pack, run = data["model"], data["pack"], data["run"]
|
|
36
36
|
info = {"label": m["label"], "served": m.get("served_name"), "tags": m.get("tags") or [],
|
|
37
37
|
"settings": data.get("settings", {}), "load_s": run.get("load_s"),
|
|
38
|
-
"peak_rss_mb": run.get("peak_rss_mb"), "
|
|
38
|
+
"peak_rss_mb": run.get("peak_rss_mb"), "vram_peak_gb": run.get("vram_peak_gb"),
|
|
39
|
+
"pack": pack["name"], "pack_fp": pack["fingerprint"],
|
|
39
40
|
# hosted (OpenRouter) models: the provider endpoint and its declared quantization
|
|
40
41
|
"endpoint": run.get("endpoint"), "quantization": run.get("quantization"),
|
|
41
42
|
"loaded_alongside": run.get("loaded_alongside") or []}
|
|
@@ -121,6 +122,12 @@ def p90(values):
|
|
|
121
122
|
return statistics.quantiles(values, n=10)[-1] if len(values) > 1 else (values[0] if values else None)
|
|
122
123
|
|
|
123
124
|
|
|
125
|
+
def pct(fraction):
|
|
126
|
+
"""A share as a percentage: "100%" and "0%" only when exact, else one decimal ("99.6%"), so a model
|
|
127
|
+
with a wrong answer never shows as perfect."""
|
|
128
|
+
return "100%" if fraction >= 1 else "0%" if fraction <= 0 else f"{100 * fraction:.1f}%"
|
|
129
|
+
|
|
130
|
+
|
|
124
131
|
def median(values):
|
|
125
132
|
values = [v for v in values if v is not None]
|
|
126
133
|
return statistics.median(values) if values else None
|
|
@@ -154,10 +161,19 @@ def settings_notes(infos):
|
|
|
154
161
|
per_model.setdefault(i["label"], i["settings"])
|
|
155
162
|
keys = sorted({k for s in per_model.values() for k in s})
|
|
156
163
|
differing = [k for k in keys if len({json.dumps(s.get(k)) for s in per_model.values()}) > 1]
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
parts = []
|
|
165
|
+
for k in differing: # only the models that differ from the most common value, named when few
|
|
166
|
+
groups = collections.defaultdict(list)
|
|
167
|
+
for m in sorted(per_model):
|
|
168
|
+
groups[json.dumps(per_model[m].get(k))].append(m)
|
|
169
|
+
common = max(groups, key=lambda v: len(groups[v]))
|
|
170
|
+
for value, ms in sorted(groups.items()):
|
|
171
|
+
if value != common:
|
|
172
|
+
who = ", ".join(ms) if len(ms) <= 3 else f"{len(ms)} models"
|
|
173
|
+
shown = "unset" if value == "null" else json.loads(value)
|
|
174
|
+
parts.append(f"{k} {shown} on {who}")
|
|
175
|
+
if parts:
|
|
176
|
+
notes.append("Settings differ: " + "; ".join(parts) + ".")
|
|
161
177
|
fps = collections.defaultdict(set)
|
|
162
178
|
for i in infos:
|
|
163
179
|
if i.get("pack"):
|
|
@@ -412,7 +428,7 @@ def by_difficulty(rows):
|
|
|
412
428
|
cells = []
|
|
413
429
|
for lvl in LEVELS:
|
|
414
430
|
x = [r["ok"] for r in sub if r.get("difficulty") == lvl]
|
|
415
|
-
cells.append(f"{sum(x)}/{len(x)} ({
|
|
431
|
+
cells.append(f"{sum(x)}/{len(x)} ({pct(sum(x) / len(x))})" if x else "-")
|
|
416
432
|
table.append([m, s] + cells)
|
|
417
433
|
return header, table
|
|
418
434
|
|
tuieval/doctor.py
CHANGED
|
@@ -10,6 +10,7 @@ import platform
|
|
|
10
10
|
import sys
|
|
11
11
|
|
|
12
12
|
from . import engine
|
|
13
|
+
from . import machines
|
|
13
14
|
from . import workspace
|
|
14
15
|
|
|
15
16
|
OK, WARN, BAD = "✓", "!", "✗"
|
|
@@ -44,6 +45,22 @@ def check(models_path=None):
|
|
|
44
45
|
return r.bad
|
|
45
46
|
r.line(OK, f"workspace {e.root}")
|
|
46
47
|
|
|
48
|
+
r.head("Machine")
|
|
49
|
+
mc = e.machine()
|
|
50
|
+
r.line(OK, f"{mc.id}: {mc.summary}")
|
|
51
|
+
if mc.discrete:
|
|
52
|
+
r.line(OK, f"models are sized to {mc.gpu_limit_gb:.0f} GB of VRAM, keeping {e.headroom_gb(mc):g} GB free "
|
|
53
|
+
"(memory_headroom_gb, gpu_memory_gb under [machines.<id>] change it)")
|
|
54
|
+
new = machines.gpu_id(machines.discrete_gpus(), mc.ram_gb)
|
|
55
|
+
if mc.id != new and not os.environ.get("EVALS_MACHINE"):
|
|
56
|
+
r.line(WARN, f"keeping the id {mc.id} this machine had before its GPU was detected, so earlier results "
|
|
57
|
+
"and tuning still match", f"to switch: EVALS_MACHINE={new} (results under the old id "
|
|
58
|
+
"then count as another machine)")
|
|
59
|
+
elif platform.system() == "Linux" and any(os.path.exists(p) for p in ("/dev/nvidia0", "/dev/kfd")):
|
|
60
|
+
r.line(WARN, "this machine seems to have an Nvidia or AMD GPU, but nvidia-smi / amd-smi / rocm-smi "
|
|
61
|
+
"isn't installed, so models are sized against system RAM",
|
|
62
|
+
"install the vendor's tools, or set gpu_memory_gb under [machines.<id>] in models.toml")
|
|
63
|
+
|
|
47
64
|
r.head("Packs")
|
|
48
65
|
for err in e.pack_errors:
|
|
49
66
|
r.line(BAD, err, "fix the pack, then: tuieval selftest")
|
tuieval/engine.py
CHANGED
|
@@ -1191,7 +1191,19 @@ class Engine:
|
|
|
1191
1191
|
|
|
1192
1192
|
# ---- serving on this machine
|
|
1193
1193
|
def machine(self):
|
|
1194
|
-
|
|
1194
|
+
"""This machine, with its models.toml [machines.<id>] gpu_memory_gb applied. A Linux machine
|
|
1195
|
+
that recorded itself before its GPU was detected keeps that id, so its results and tuning
|
|
1196
|
+
still match (EVALS_MACHINE renames it)."""
|
|
1197
|
+
mc = machines.detect()
|
|
1198
|
+
if mc.discrete and not os.environ.get("EVALS_MACHINE"):
|
|
1199
|
+
old = machines.machine_id(mc.chip, mc.ram_gb)
|
|
1200
|
+
if os.path.isdir(os.path.join(self.tuning_dir, old)) and \
|
|
1201
|
+
not os.path.isdir(os.path.join(self.tuning_dir, mc.id)):
|
|
1202
|
+
mc = dataclasses.replace(mc, id=old)
|
|
1203
|
+
override = self.cfg.get("machines", {}).get(mc.id, {}).get("gpu_memory_gb")
|
|
1204
|
+
if override:
|
|
1205
|
+
mc = dataclasses.replace(mc, gpu_limit_gb=float(override))
|
|
1206
|
+
return mc
|
|
1195
1207
|
|
|
1196
1208
|
def machines(self):
|
|
1197
1209
|
"""This machine first, then every other machine recorded in tuning/ (synced)."""
|
|
@@ -1203,9 +1215,14 @@ class Engine:
|
|
|
1203
1215
|
return [here] + [mc for mc in profiles.known_machines(self.tuning_dir) if mc.id != here.id]
|
|
1204
1216
|
|
|
1205
1217
|
def machine_settings(self, machine_id=None):
|
|
1206
|
-
"""models.toml [machines.<id>] (optional): memory_headroom_gb, name."""
|
|
1218
|
+
"""models.toml [machines.<id>] (optional): memory_headroom_gb, gpu_memory_gb, name."""
|
|
1207
1219
|
return self.cfg.get("machines", {}).get(machine_id or self.machine().id, {})
|
|
1208
1220
|
|
|
1221
|
+
def headroom_gb(self, mc):
|
|
1222
|
+
"""GPU memory kept free: memory_headroom_gb, else 4 GB on a Mac (macOS and other apps share
|
|
1223
|
+
it) and 1.5 GB in a discrete GPU's VRAM."""
|
|
1224
|
+
return float(self.machine_settings(mc.id).get("memory_headroom_gb", 1.5 if mc.discrete else 4.0))
|
|
1225
|
+
|
|
1209
1226
|
def parallel_models(self):
|
|
1210
1227
|
"""How many models a run serves at a time on this machine: models.toml [machines.<id>]
|
|
1211
1228
|
parallel_models (default 1)."""
|
|
@@ -1224,7 +1241,7 @@ class Engine:
|
|
|
1224
1241
|
def memory_available_gb(self):
|
|
1225
1242
|
"""GPU memory models may take here (what the fit check sizes contexts against)."""
|
|
1226
1243
|
mc = self.machine()
|
|
1227
|
-
return mc.gpu_limit_gb - self.
|
|
1244
|
+
return mc.gpu_limit_gb - self.headroom_gb(mc)
|
|
1228
1245
|
|
|
1229
1246
|
def runs_alone(self, m):
|
|
1230
1247
|
"""A server whose before_start step may stop other servers never runs next to another model."""
|
|
@@ -1269,8 +1286,7 @@ class Engine:
|
|
|
1269
1286
|
if fit_check(server):
|
|
1270
1287
|
mmproj = expand(m.get("mmproj", ""))
|
|
1271
1288
|
extra = os.path.getsize(mmproj) if mmproj and os.path.isfile(mmproj) else 0
|
|
1272
|
-
|
|
1273
|
-
f = machines.fit(path, mc, kv_type=kv or "f16", headroom_gb=headroom, extra_bytes=extra,
|
|
1289
|
+
f = machines.fit(path, mc, kv_type=kv or "f16", headroom_gb=self.headroom_gb(mc), extra_bytes=extra,
|
|
1274
1290
|
want_ctx=want)
|
|
1275
1291
|
ctx, fits, note, need = f.max_ctx, f.fits, f.note, f.need_gb
|
|
1276
1292
|
except (OSError, ValueError) as e:
|
|
@@ -1484,11 +1500,22 @@ class Engine:
|
|
|
1484
1500
|
time.sleep(0.2)
|
|
1485
1501
|
threading.Thread(target=run, daemon=True).start()
|
|
1486
1502
|
|
|
1487
|
-
|
|
1503
|
+
VRAM_EVERY_S = 6 # how often to read a discrete GPU's VRAM while a server runs
|
|
1504
|
+
|
|
1505
|
+
def _watch_memory(self, proc, label, stop, peak, info=None):
|
|
1488
1506
|
"""Peak resident memory of the server's process group (MB). On Apple Silicon this includes
|
|
1489
|
-
the model weights mapped for the GPU.
|
|
1507
|
+
the model weights mapped for the GPU. With discrete GPUs, also the peak VRAM it holds
|
|
1508
|
+
(info["vram_peak_gb"]): its own processes' on Nvidia, the machine's on AMD."""
|
|
1509
|
+
discrete = info is not None and self.machine().discrete
|
|
1510
|
+
last_vram = [0.0]
|
|
1511
|
+
|
|
1490
1512
|
def run():
|
|
1491
1513
|
while not stop.is_set():
|
|
1514
|
+
if discrete and time.time() - last_vram[0] >= self.VRAM_EVERY_S:
|
|
1515
|
+
last_vram[0] = time.time()
|
|
1516
|
+
used = machines.vram_used_gb(machines.tree_pids(proc.pid))
|
|
1517
|
+
if used:
|
|
1518
|
+
info["vram_peak_gb"] = round(max(info.get("vram_peak_gb") or 0, used), 2)
|
|
1492
1519
|
try:
|
|
1493
1520
|
out = subprocess.run(["ps", "-axo", "pgid=,rss="], capture_output=True, text=True).stdout
|
|
1494
1521
|
rss = sum(int(r) for g, r in (l.split() for l in out.splitlines() if l.strip())
|
|
@@ -1789,7 +1816,7 @@ class Engine:
|
|
|
1789
1816
|
f"(see logs/server/{log_name or m['label']}.log)")
|
|
1790
1817
|
t0 = time.time()
|
|
1791
1818
|
srv = self._start(cmd, log, cwd=cwd)
|
|
1792
|
-
self._watch_memory(srv, m["label"], stop, peak)
|
|
1819
|
+
self._watch_memory(srv, m["label"], stop, peak, info)
|
|
1793
1820
|
else:
|
|
1794
1821
|
key_env = server.get("api_key_env")
|
|
1795
1822
|
if key_env and not os.environ.get(key_env):
|
|
@@ -1881,6 +1908,7 @@ class Engine:
|
|
|
1881
1908
|
def run_info():
|
|
1882
1909
|
return {"load_s": info["load_s"], "peak_rss_mb": info["peak_mb"](),
|
|
1883
1910
|
"server_facts": info["facts"] or None, "gpu_peak_gb": info.get("gpu_peak_gb"),
|
|
1911
|
+
"vram_peak_gb": info.get("vram_peak_gb"),
|
|
1884
1912
|
"kernel_share_max": info.get("kernel_share_max"),
|
|
1885
1913
|
**({"loaded_alongside": loaded_alongside} if loaded_alongside else {}), **serving_info}
|
|
1886
1914
|
failed_in_row = 0
|
|
@@ -1965,7 +1993,8 @@ class Engine:
|
|
|
1965
1993
|
return tests
|
|
1966
1994
|
|
|
1967
1995
|
# ---- sittings: a pack run over several sessions (stopped and resumed) keeps its total time
|
|
1968
|
-
SITTING_KEYS = ("load_s", "peak_rss_mb", "gpu_peak_gb", "
|
|
1996
|
+
SITTING_KEYS = ("load_s", "peak_rss_mb", "gpu_peak_gb", "vram_peak_gb", "kernel_share_max", "machine",
|
|
1997
|
+
"loaded_alongside")
|
|
1969
1998
|
|
|
1970
1999
|
def _sitting(self, job, info):
|
|
1971
2000
|
now = time.time()
|
|
@@ -2037,7 +2066,7 @@ class Engine:
|
|
|
2037
2066
|
out = {"started": min(s["started"] for s in sittings),
|
|
2038
2067
|
"wall_s": round(sum(s.get("wall_s") or 0 for s in sittings), 1),
|
|
2039
2068
|
"sittings": len(sittings), "sittings_log": sittings}
|
|
2040
|
-
for k in ("peak_rss_mb", "gpu_peak_gb", "kernel_share_max"):
|
|
2069
|
+
for k in ("peak_rss_mb", "gpu_peak_gb", "vram_peak_gb", "kernel_share_max"):
|
|
2041
2070
|
if peak(k) is not None:
|
|
2042
2071
|
out[k] = peak(k)
|
|
2043
2072
|
along = sorted({l for s in sittings for l in s.get("loaded_alongside") or []})
|
tuieval/machines.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Which machine this is, what a model needs, and whether it fits.
|
|
2
2
|
|
|
3
|
-
detect() -> Machine (chip, cores, RAM, GPU memory limit, id like "m3max-64gb"
|
|
3
|
+
detect() -> Machine (chip, cores, RAM, GPU memory limit, id like "m3max-64gb";
|
|
4
|
+
with Nvidia/AMD cards on Linux, their VRAM and an id like "rtx4090-128gb")
|
|
4
5
|
read_gguf(path) -> GGUF header facts (architecture, layers, KV heads, context length, …)
|
|
5
6
|
fit(model_path, …) -> Fit (largest context that fits this machine, estimated memory)
|
|
6
7
|
|
|
@@ -9,6 +10,7 @@ the KV cache on its own (that would change answers); a quantized-KV variant is a
|
|
|
9
10
|
"""
|
|
10
11
|
import dataclasses
|
|
11
12
|
import functools
|
|
13
|
+
import json
|
|
12
14
|
import os
|
|
13
15
|
import platform
|
|
14
16
|
import re
|
|
@@ -28,15 +30,19 @@ class Machine:
|
|
|
28
30
|
id: str
|
|
29
31
|
chip: str
|
|
30
32
|
ram_gb: float
|
|
31
|
-
gpu_limit_gb: float # what Metal may wire for the GPU
|
|
33
|
+
gpu_limit_gb: float # what Metal may wire for the GPU; with discrete GPUs, their VRAM (all cards)
|
|
32
34
|
p_cores: int
|
|
33
35
|
e_cores: int
|
|
34
36
|
gpu_cores: int | None
|
|
35
37
|
os: str
|
|
38
|
+
gpu_name: str | None = None # discrete GPUs, e.g. "2× NVIDIA GeForce RTX 4090"
|
|
39
|
+
discrete: bool = False # the GPU has its own memory (VRAM), separate from RAM
|
|
36
40
|
|
|
37
41
|
@property
|
|
38
42
|
def summary(self):
|
|
39
43
|
cores = f"{self.p_cores}P+{self.e_cores}E" if self.e_cores else f"{self.p_cores} cores"
|
|
44
|
+
if self.discrete:
|
|
45
|
+
return f"{self.chip} · {self.ram_gb:.0f} GB RAM · {self.gpu_name} · {self.gpu_limit_gb:.0f} GB VRAM · {cores}"
|
|
40
46
|
gpu = f", {self.gpu_cores}-core GPU" if self.gpu_cores else ""
|
|
41
47
|
return f"{self.chip} · {self.ram_gb:.0f} GB (GPU up to {self.gpu_limit_gb:.0f} GB) · {cores}{gpu}"
|
|
42
48
|
|
|
@@ -53,6 +59,129 @@ def machine_id(chip, ram_gb):
|
|
|
53
59
|
return re.sub(r"[^a-z0-9]+", "", name.lower()) + f"-{round(ram_gb)}gb"
|
|
54
60
|
|
|
55
61
|
|
|
62
|
+
def gpu_id(gpus, ram_gb):
|
|
63
|
+
"""A machine id from its discrete GPUs: "rtx4090-128gb", "2xrtx4090-128gb", "rx7900xtx+rtx3090-64gb"."""
|
|
64
|
+
names = []
|
|
65
|
+
for name in sorted({n for n, _ in gpus}):
|
|
66
|
+
short = re.sub(r"\b(nvidia|geforce|amd|radeon|ati|graphics|tm)\b", "", name.lower())
|
|
67
|
+
short = re.sub(r"[^a-z0-9]+", "", short) or "gpu"
|
|
68
|
+
count = sum(n == name for n, _ in gpus)
|
|
69
|
+
names.append(f"{count}x{short}" if count > 1 else short)
|
|
70
|
+
return "+".join(names) + f"-{round(ram_gb)}gb"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------- discrete GPUs (Linux: Nvidia, AMD)
|
|
74
|
+
# Read from the vendors' own tools. Anything unexpected counts as "not detected", never an error.
|
|
75
|
+
def _run(cmd, timeout=10):
|
|
76
|
+
try:
|
|
77
|
+
p = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
78
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
79
|
+
return None
|
|
80
|
+
return p.stdout if p.returncode == 0 else None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _nvidia_gpus():
|
|
84
|
+
out = _run(["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader,nounits"])
|
|
85
|
+
gpus = []
|
|
86
|
+
for line in (out or "").splitlines():
|
|
87
|
+
name, _, mib = line.rpartition(",")
|
|
88
|
+
try:
|
|
89
|
+
gpus.append((name.strip(), float(mib) / 1024))
|
|
90
|
+
except ValueError:
|
|
91
|
+
pass
|
|
92
|
+
return [g for g in gpus if g[0] and g[1] > 0]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _walk(obj):
|
|
96
|
+
"""Every dict inside a JSON value."""
|
|
97
|
+
if isinstance(obj, dict):
|
|
98
|
+
yield obj
|
|
99
|
+
for v in obj.values():
|
|
100
|
+
yield from _walk(v)
|
|
101
|
+
elif isinstance(obj, list):
|
|
102
|
+
for v in obj:
|
|
103
|
+
yield from _walk(v)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _bytes(value, unit=None):
|
|
107
|
+
"""VRAM sizes as amd-smi/rocm-smi print them: 25753026560, "24560 MB", {"value": 24560, "unit": "MB"}."""
|
|
108
|
+
if isinstance(value, dict):
|
|
109
|
+
return _bytes(value.get("value"), value.get("unit"))
|
|
110
|
+
m = re.match(r"\s*([\d.]+)\s*([KMGT]i?B|B)?\s*$", str(value), re.I)
|
|
111
|
+
if not m:
|
|
112
|
+
return None
|
|
113
|
+
scale = {"B": 1, "KB": 2**10, "MB": 2**20, "GB": 2**30, "TB": 2**40}
|
|
114
|
+
u = (m.group(2) or unit or "B").upper().replace("I", "")
|
|
115
|
+
return float(m.group(1)) * scale.get(u, 1)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _amd_gpus():
|
|
119
|
+
out = _run(["amd-smi", "static", "--asic", "--vram", "--json"])
|
|
120
|
+
gpus = []
|
|
121
|
+
try:
|
|
122
|
+
for d in _walk(json.loads(out)) if out else []:
|
|
123
|
+
if "vram" in d and isinstance(d["vram"], dict):
|
|
124
|
+
size = _bytes(d["vram"].get("size"))
|
|
125
|
+
name = (d.get("asic") or {}).get("market_name") or "AMD GPU"
|
|
126
|
+
if size:
|
|
127
|
+
gpus.append((str(name), size / GB))
|
|
128
|
+
except ValueError:
|
|
129
|
+
gpus = []
|
|
130
|
+
if gpus:
|
|
131
|
+
return gpus
|
|
132
|
+
out = _run(["rocm-smi", "--showmeminfo", "vram", "--showproductname", "--json"])
|
|
133
|
+
try:
|
|
134
|
+
cards = json.loads(out) if out else {}
|
|
135
|
+
except ValueError:
|
|
136
|
+
return []
|
|
137
|
+
for key, d in sorted(cards.items()) if isinstance(cards, dict) else []:
|
|
138
|
+
if not key.startswith("card") or not isinstance(d, dict):
|
|
139
|
+
continue
|
|
140
|
+
size = _bytes(d.get("VRAM Total Memory (B)"))
|
|
141
|
+
name = d.get("Card Series") or d.get("Card series") or d.get("Card SKU") or d.get("Card model") or "AMD GPU"
|
|
142
|
+
if size:
|
|
143
|
+
gpus.append((str(name), size / GB))
|
|
144
|
+
return gpus
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def discrete_gpus():
|
|
148
|
+
"""[(name, VRAM GB)] for the Nvidia or AMD cards this machine has, or [] (none, or tools missing)."""
|
|
149
|
+
return _nvidia_gpus() or _amd_gpus()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def vram_used_gb(pids=None):
|
|
153
|
+
"""VRAM in use now (GB): by these processes on Nvidia (all of it when pids is None), or on the
|
|
154
|
+
whole machine on AMD, whose tools don't report it reliably per process. None if unknown."""
|
|
155
|
+
if pids is not None:
|
|
156
|
+
out = _run(["nvidia-smi", "--query-compute-apps=pid,used_memory", "--format=csv,noheader,nounits"])
|
|
157
|
+
if out is not None:
|
|
158
|
+
used = 0.0
|
|
159
|
+
for line in out.splitlines():
|
|
160
|
+
pid, _, mib = line.partition(",")
|
|
161
|
+
if pid.strip() in {str(p) for p in pids}:
|
|
162
|
+
try:
|
|
163
|
+
used += float(mib) / 1024
|
|
164
|
+
except ValueError:
|
|
165
|
+
pass
|
|
166
|
+
return used
|
|
167
|
+
out = _run(["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"])
|
|
168
|
+
if out is not None:
|
|
169
|
+
try:
|
|
170
|
+
return sum(float(x) for x in out.split()) / 1024
|
|
171
|
+
except ValueError:
|
|
172
|
+
return None
|
|
173
|
+
out = _run(["rocm-smi", "--showmeminfo", "vram", "--json"])
|
|
174
|
+
try:
|
|
175
|
+
cards = json.loads(out) if out else None
|
|
176
|
+
except ValueError:
|
|
177
|
+
return None
|
|
178
|
+
if not isinstance(cards, dict):
|
|
179
|
+
return None
|
|
180
|
+
used = [_bytes(d.get("VRAM Total Used Memory (B)")) for k, d in cards.items()
|
|
181
|
+
if k.startswith("card") and isinstance(d, dict)]
|
|
182
|
+
return sum(u for u in used if u) / GB if any(used) else None
|
|
183
|
+
|
|
184
|
+
|
|
56
185
|
@functools.lru_cache(maxsize=1)
|
|
57
186
|
def detect():
|
|
58
187
|
"""This machine. Override the id with EVALS_MACHINE if two machines would collide."""
|
|
@@ -73,11 +202,19 @@ def detect():
|
|
|
73
202
|
except (OSError, subprocess.TimeoutExpired):
|
|
74
203
|
pass
|
|
75
204
|
os_name = "macOS " + platform.mac_ver()[0]
|
|
76
|
-
else: # Linux and others: CPU
|
|
205
|
+
else: # Linux and others: the CPU side, plus Nvidia/AMD cards if their tools are installed
|
|
77
206
|
chip = platform.processor() or platform.machine()
|
|
78
207
|
ram = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") / GB
|
|
79
208
|
p, e, gpu_cores, limit = os.cpu_count() or 1, 0, None, ram * 0.9
|
|
80
209
|
os_name = platform.system()
|
|
210
|
+
gpus = discrete_gpus()
|
|
211
|
+
if gpus:
|
|
212
|
+
names = sorted({n for n, _ in gpus})
|
|
213
|
+
gpu_name = " + ".join(f"{sum(n == x for n, _ in gpus)}× {x}" if sum(n == x for n, _ in gpus) > 1 else x
|
|
214
|
+
for x in names)
|
|
215
|
+
mid = os.environ.get("EVALS_MACHINE") or gpu_id(gpus, ram)
|
|
216
|
+
return Machine(mid, chip, round(ram, 1), round(sum(g for _, g in gpus), 1), p, e, None, os_name,
|
|
217
|
+
gpu_name=gpu_name, discrete=True)
|
|
81
218
|
mid = os.environ.get("EVALS_MACHINE") or machine_id(chip, ram)
|
|
82
219
|
return Machine(mid, chip, round(ram, 1), round(limit, 1), p, e, gpu_cores, os_name)
|
|
83
220
|
|
|
@@ -195,14 +332,15 @@ def fit(model_path, machine=None, kv_type="f16", headroom_gb=4.0, extra_bytes=0,
|
|
|
195
332
|
model_max = info["context_length"] or 131072
|
|
196
333
|
max_ctx = int(min(room / per_tok, model_max, want_ctx or model_max)) if room > 0 else 0
|
|
197
334
|
max_ctx = max_ctx // 1024 * 1024
|
|
335
|
+
of = " of VRAM" if machine.discrete else ""
|
|
198
336
|
if max_ctx < MIN_CTX:
|
|
199
337
|
need = fixed + MIN_CTX * per_tok / GB
|
|
200
338
|
return Fit(False, max_ctx, round(need, 1), round(available, 1),
|
|
201
339
|
f"doesn't fit on {machine.id}: needs ~{need:.0f} GB at {MIN_CTX // 1024}k context, "
|
|
202
|
-
f"{available:.0f} GB available")
|
|
340
|
+
f"{available:.0f} GB{of} available")
|
|
203
341
|
need = fixed + max_ctx * per_tok / GB
|
|
204
342
|
return Fit(True, max_ctx, round(need, 1), round(available, 1),
|
|
205
|
-
f"fits on {machine.id} up to {max_ctx // 1024}k context (~{need:.0f} of {available:.0f} GB)")
|
|
343
|
+
f"fits on {machine.id} up to {max_ctx // 1024}k context (~{need:.0f} of {available:.0f} GB{of})")
|
|
206
344
|
|
|
207
345
|
|
|
208
346
|
# ---------------------------------------------------------------- memory pressure
|
|
@@ -287,6 +425,26 @@ def tree_cpu_seconds(pid):
|
|
|
287
425
|
return (sum(rows[p][1] for p in tree), sum(rows[p][2] for p in tree))
|
|
288
426
|
|
|
289
427
|
|
|
428
|
+
def tree_pids(pid):
|
|
429
|
+
"""A process and all its descendants (pids), or [pid] if ps can't be read."""
|
|
430
|
+
try:
|
|
431
|
+
out = subprocess.run(["ps", "-axo", "pid=,ppid="], capture_output=True, text=True, timeout=5).stdout
|
|
432
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
433
|
+
return [pid]
|
|
434
|
+
children = {}
|
|
435
|
+
for line in out.splitlines():
|
|
436
|
+
parts = line.split()
|
|
437
|
+
if len(parts) == 2:
|
|
438
|
+
children.setdefault(parts[1], []).append(parts[0])
|
|
439
|
+
tree, frontier = {str(pid)}, [str(pid)]
|
|
440
|
+
while frontier:
|
|
441
|
+
for child in children.get(frontier.pop(), []):
|
|
442
|
+
if child not in tree:
|
|
443
|
+
tree.add(child)
|
|
444
|
+
frontier.append(child)
|
|
445
|
+
return sorted(int(p) for p in tree)
|
|
446
|
+
|
|
447
|
+
|
|
290
448
|
def group_cpu_seconds(pgid):
|
|
291
449
|
"""(user, kernel) CPU seconds used so far by a process group, or None."""
|
|
292
450
|
try:
|
tuieval/pta.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
"""PTA index: parsimony
|
|
2
|
-
as a bar per score for each model.
|
|
1
|
+
"""PTA index: parsimony (tokens), time and accuracy per model, a bar each (longer = better).
|
|
3
2
|
|
|
4
|
-
P parsimony
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
P parsimony (tokens) the tokens (reasoning and answer) used on every question compared (each
|
|
4
|
+
question at its median over repeats), shown as how many times the leanest
|
|
5
|
+
model's ("2.4×"). Unlike T it doesn't depend on the machine.
|
|
6
|
+
T time the total time to answer those questions, as how many times the fastest
|
|
7
|
+
model's ("28×").
|
|
8
|
+
A accuracy the share of answers to those questions that passed (%).
|
|
9
|
+
|
|
10
|
+
Behind the P and T bars is a 0-100 score: 100 for the best, POINTS_PER_DOUBLING points less for every
|
|
11
|
+
doubling (0 at about 100 times more), so models far behind the best still get a visible bar.
|
|
11
12
|
|
|
12
13
|
Models are compared on the questions all of them answered, so one that skipped some never looks
|
|
13
14
|
faster. Server and connection errors aren't answers. By default a model with fewer than half as many
|
|
@@ -32,12 +33,23 @@ def score(total, best):
|
|
|
32
33
|
return max(0.0, 100 - POINTS_PER_DOUBLING * math.log2(total / best))
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
def
|
|
36
|
+
def memory_gb(infos):
|
|
37
|
+
"""{model: the most memory its server held in any run (GB)}, for models tuieval served (hosted: none).
|
|
38
|
+
On a discrete GPU that's its peak VRAM (vram_peak_gb), where the model lives; otherwise its resident memory."""
|
|
39
|
+
out = {}
|
|
40
|
+
for i in infos:
|
|
41
|
+
gb = i.get("vram_peak_gb") or (i["peak_rss_mb"] / 1024 if i.get("peak_rss_mb") else None)
|
|
42
|
+
if gb:
|
|
43
|
+
out[i["label"]] = max(out.get(i["label"], 0), gb)
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def index(rows, models=None, memory=None):
|
|
36
48
|
"""{"questions": n shared, "models": [{model, P, T, A, tokens, total_s, passed, answers, alongside,
|
|
37
|
-
fewest, fastest}],
|
|
49
|
+
fewest, fastest, tps, ttft, memory_gb}],
|
|
38
50
|
"left_out": [(model, questions answered)], "no_answers": [model], "most": questions answered by the
|
|
39
51
|
most-answered model}, best accuracy first. models: the ones to compare (default: every model in rows
|
|
40
|
-
with enough answers; models named here are compared whatever their count)."""
|
|
52
|
+
with enough answers; models named here are compared whatever their count). memory: memory_gb()."""
|
|
41
53
|
no_answers = sorted({r["model"] for r in rows} - {r["model"] for r in rows if not r.get("error")})
|
|
42
54
|
rows = [r for r in rows if not r.get("error")] # a server or connection error isn't an answer
|
|
43
55
|
tests = compare.per_question(rows)
|
|
@@ -61,12 +73,19 @@ def index(rows, models=None):
|
|
|
61
73
|
"total_s": sum(c["secs"] or 0 for c in cells),
|
|
62
74
|
"passed": sum(c["passed"] for c in cells), "answers": answers,
|
|
63
75
|
"A": 100 * sum(c["passed"] for c in cells) / answers if answers else None,
|
|
76
|
+
# generation speed and time to first token over the same answers (medians)
|
|
77
|
+
"tps": compare.median(r.get("gen_tps") for c in cells for r in c["rows"]),
|
|
78
|
+
"ttft": compare.median(r.get("ttft") for c in cells for r in c["rows"]),
|
|
79
|
+
"memory_gb": (memory or {}).get(m),
|
|
64
80
|
# answers that ran while other models were served shared the machine's speed
|
|
65
81
|
"alongside": any(r.get("alongside") for c in cells for r in c["rows"])})
|
|
66
82
|
fastest = min((x["total_s"] for x in out if x["total_s"] > 0), default=None)
|
|
67
83
|
fewest = min((x["tokens"] for x in out if x["tokens"] > 0), default=None)
|
|
68
84
|
for x in out:
|
|
69
85
|
x["P"], x["T"] = score(x["tokens"], fewest), score(x["total_s"], fastest)
|
|
86
|
+
# shown on the bars: how many times the leanest / fastest model's total (1.0 = the best)
|
|
87
|
+
x["tokens_x"] = x["tokens"] / fewest if fewest and x["tokens"] else None
|
|
88
|
+
x["time_x"] = x["total_s"] / fastest if fastest and x["total_s"] else None
|
|
70
89
|
x["fewest"] = bool(fewest) and x["tokens"] == fewest
|
|
71
90
|
x["fastest"] = bool(fastest) and x["total_s"] == fastest
|
|
72
91
|
out.sort(key=lambda x: (-(x["A"] or 0), -(x["T"] or 0), x["model"]))
|
|
@@ -74,29 +93,40 @@ def index(rows, models=None):
|
|
|
74
93
|
"most": most}
|
|
75
94
|
|
|
76
95
|
|
|
77
|
-
|
|
96
|
+
# (letter, name, what the number beside the bar shows, colour); the bar itself is the 0-100 score
|
|
97
|
+
LETTERS = (("P", "parsimony (tokens)", "tokens_x", "cyan"), ("T", "time", "time_x", "magenta"),
|
|
98
|
+
("A", "accuracy", "A", "green"))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def times(v):
|
|
102
|
+
"""A multiple of the best: "1.0×", "2.4×", "28×"."""
|
|
103
|
+
return "-" if v is None else f"{v:.1f}×" if v < 9.95 else f"{v:.0f}×"
|
|
78
104
|
|
|
79
105
|
|
|
80
106
|
def bars(result, width=20, markup=True):
|
|
81
|
-
"""Lines with a row per model and a bar per score (P, T, A), longer = better
|
|
82
|
-
the
|
|
107
|
+
"""Lines with a row per model and a bar per score (P, T, A), longer = better: the at-a-glance view.
|
|
108
|
+
Beside P and T, how many times the leanest / fastest model's tokens / time; beside A, the percentage.
|
|
109
|
+
markup=False gives plain text (the report)."""
|
|
83
110
|
if not result["models"]:
|
|
84
111
|
return []
|
|
85
112
|
name_w = max(len(x["model"]) for x in result["models"])
|
|
86
|
-
|
|
113
|
+
# each column: the bar, a space, "100%" or "9.8×" (5), then a gap; at least as wide as its heading
|
|
114
|
+
cell_w = max(width + 9, *(len(f"{k} {name}") + 2 for k, name, _, _ in LETTERS))
|
|
87
115
|
head = " " * (name_w + 2) + "".join(f"{f'{k} {name}':<{cell_w}}" for k, name, _, _ in LETTERS)
|
|
88
116
|
lines = [f"[b]{head.rstrip()}[/b]" if markup else head.rstrip()]
|
|
89
117
|
for x in result["models"]:
|
|
90
118
|
cells = []
|
|
91
|
-
for k, _,
|
|
119
|
+
for k, _, shown, color in LETTERS:
|
|
92
120
|
v = x[k]
|
|
93
121
|
full = 0 if v is None else round(width * max(0, min(100, v)) / 100)
|
|
122
|
+
if v is not None and v < 100: # full only when perfect (accuracy) or the best (P, T)
|
|
123
|
+
full = min(full, width - 1)
|
|
94
124
|
bar, rest = "█" * full, "░" * (width - full)
|
|
95
|
-
num = "-" if v is None else
|
|
125
|
+
num = (("-" if v is None else compare.pct(v / 100)) if shown == "A" else times(x.get(shown)))
|
|
96
126
|
bar = f"[{color}]{bar}[/][grey37]{rest}[/]" if markup else bar + rest
|
|
97
|
-
cells.append(f"{bar} {num:>
|
|
127
|
+
cells.append(f"{bar} {num:>6}" + " " * (cell_w - width - 7))
|
|
98
128
|
name = f"{x['model']:<{name_w}}"
|
|
99
|
-
lines.append(f"{rich_escape(name) if markup else name} " + "
|
|
129
|
+
lines.append((f"{rich_escape(name) if markup else name} " + "".join(cells)).rstrip())
|
|
100
130
|
return lines
|
|
101
131
|
|
|
102
132
|
|
|
@@ -116,22 +146,28 @@ def left_out_lines(result):
|
|
|
116
146
|
return out
|
|
117
147
|
|
|
118
148
|
|
|
119
|
-
def _score(v, unit=""):
|
|
120
|
-
"""A is a percentage (%), P and T scores out of 100 (/100: on a log scale, not percentages)."""
|
|
121
|
-
return "-" if v is None else f"{v:.0f}{unit}"
|
|
122
|
-
|
|
123
|
-
|
|
124
149
|
def table(result):
|
|
125
150
|
"""(header, rows) for a plain table of the index."""
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
rows = [[x["model"],
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
151
|
+
n = result["questions"] or 1
|
|
152
|
+
header = ["model", "tokens/answer", "time/answer", "answers right", "tok/s", "TTFT s", "memory GB"]
|
|
153
|
+
rows = [[x["model"], f"{round(x['tokens'] / n):,}" + (" ★" if x.get("fewest") else ""),
|
|
154
|
+
fmt_secs(x["total_s"] / n) + (" ★" if x.get("fastest") else ""),
|
|
155
|
+
f"{x['passed']}/{x['answers']}", _num(x.get("tps"), ".0f"), _num(x.get("ttft"), ".1f"),
|
|
156
|
+
_num(x.get("memory_gb"), ".1f")] for x in result["models"]]
|
|
132
157
|
return header, rows
|
|
133
158
|
|
|
134
159
|
|
|
160
|
+
def fmt_secs(secs):
|
|
161
|
+
"""Time per answer: "2.5s", "69s", "4m10s"."""
|
|
162
|
+
if not secs:
|
|
163
|
+
return "-"
|
|
164
|
+
return f"{secs:.1f}s" if secs < 10 else f"{secs:.0f}s" if secs < 60 else fmt_total(secs)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _num(v, fmt):
|
|
168
|
+
return "-" if v is None else format(v, fmt)
|
|
169
|
+
|
|
170
|
+
|
|
135
171
|
def fmt_total(secs):
|
|
136
172
|
if not secs:
|
|
137
173
|
return "-"
|
|
@@ -147,19 +183,18 @@ def scope_note(result, models):
|
|
|
147
183
|
if not result["questions"]:
|
|
148
184
|
return "These models have no question in common yet, so there's nothing to compare."
|
|
149
185
|
return (f"Compared on the {result['questions']} questions all {len(result['models'])} model(s) answered. "
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
"model added lowers the others.")
|
|
186
|
+
"Longer bars are better. P parsimony (tokens) and T time: how many times the leanest / fastest "
|
|
187
|
+
"model's (1.0×, ★ in the table). A accuracy: answers right.")
|
|
153
188
|
|
|
154
189
|
|
|
155
190
|
def for_engine(e, labels=None, packs=None, results_dir=None):
|
|
156
191
|
"""(result, models) from a workspace's finished results (results on their way out excluded)."""
|
|
157
192
|
paths = [p for p in compare.default_paths(results_dir or e.results_dir) if not e.superseded(p)]
|
|
158
|
-
rows,
|
|
193
|
+
rows, infos = compare.load(paths) if paths else ([], [])
|
|
159
194
|
if packs:
|
|
160
195
|
rows = [r for r in rows if r["suite"] in packs]
|
|
161
196
|
models = sorted({r["model"] for r in rows} & set(labels) if labels else {r["model"] for r in rows})
|
|
162
|
-
return index(rows, models if labels else None), models
|
|
197
|
+
return index(rows, models if labels else None, memory_gb(infos)), models
|
|
163
198
|
|
|
164
199
|
|
|
165
200
|
def markdown(result, models):
|
tuieval/run_evals.py
CHANGED
|
@@ -373,11 +373,12 @@ def report_markdown(e, labels=None):
|
|
|
373
373
|
+ f"; truncated {100 * ev['truncation']:.1f}%"
|
|
374
374
|
+ (f"; consistency {100 * ev['consistency']:.0f}% of {ev['groups']} groups" if ev.get("consistency") is not None else ""))
|
|
375
375
|
recs = e.records(label, name)
|
|
376
|
+
from .compare import pct
|
|
376
377
|
levels = []
|
|
377
378
|
for lvl in ("easy", "medium", "hard"):
|
|
378
379
|
x = [r["pass"] for r in recs if r.get("difficulty") == lvl]
|
|
379
380
|
if x:
|
|
380
|
-
levels.append(f"{lvl} {sum(x)}/{len(x)} ({
|
|
381
|
+
levels.append(f"{lvl} {sum(x)}/{len(x)} ({pct(sum(x) / len(x))})")
|
|
381
382
|
if levels:
|
|
382
383
|
lines.append(" - by difficulty: " + ", ".join(levels))
|
|
383
384
|
for ex in ev.get("critical_examples", []):
|
|
@@ -389,7 +390,7 @@ def report_markdown(e, labels=None):
|
|
|
389
390
|
|
|
390
391
|
def cmd_pta(argv):
|
|
391
392
|
p = argparse.ArgumentParser(prog="tuieval pta",
|
|
392
|
-
description="The PTA index: parsimony (tokens),
|
|
393
|
+
description="The PTA index: parsimony (tokens), time and accuracy per model, as a "
|
|
393
394
|
"bar per score for each model and a table, over the questions all the models answered.")
|
|
394
395
|
p.add_argument("--only", help="comma-separated model labels (default: every model with results)")
|
|
395
396
|
p.add_argument("--packs", help="comma-separated packs (default: all)")
|
|
@@ -413,7 +414,7 @@ def cmd_pta(argv):
|
|
|
413
414
|
return
|
|
414
415
|
out.print()
|
|
415
416
|
for line in pta.bars(result, a.width):
|
|
416
|
-
out.print(line)
|
|
417
|
+
out.print(line, soft_wrap=True) # a bar row stays one line (the terminal wraps it if it must)
|
|
417
418
|
header, rows = pta.table(result)
|
|
418
419
|
widths = [max(len(h), *(len(r[i]) for r in rows)) for i, h in enumerate(header)]
|
|
419
420
|
out.print()
|
tuieval/tui.py
CHANGED
|
@@ -10,7 +10,7 @@ Run screen: live reasoning and answer, per-test ✓/✗, TTFT and tokens/s, p
|
|
|
10
10
|
k skips the current model, c cancels everything, f pauses auto-scroll.
|
|
11
11
|
r shows results at any time (also mid-run); n starts a new run when it ends.
|
|
12
12
|
Enter on a Recent results row shows that answer in full.
|
|
13
|
-
Results: opens on the PTA index (parsimony,
|
|
13
|
+
Results: opens on the PTA index (parsimony, time and accuracy per model, as bars). Per
|
|
14
14
|
question compares each model's tokens and seconds on the same question. ctrl+r
|
|
15
15
|
reloads (it also reloads when a run's pack finishes). Enter on a Failures row or a
|
|
16
16
|
Per question cell shows the answer: grading and checks, reasoning, answer, question.
|
|
@@ -19,6 +19,7 @@ Anywhere: esc/q go back (q quits only on Setup), ctrl+q quits, w lists this
|
|
|
19
19
|
tunes. Leaving a live run or tune keeps it going; the header shows its progress.
|
|
20
20
|
"""
|
|
21
21
|
import argparse
|
|
22
|
+
import collections
|
|
22
23
|
import glob
|
|
23
24
|
import json
|
|
24
25
|
import os
|
|
@@ -331,16 +332,15 @@ with the model's reasoning and answer as they stream, then recent results with t
|
|
|
331
332
|
v with several models at a time: stream the next one (k skips the one streaming)""",
|
|
332
333
|
"ResultsScreen": """[b]Results[/b]
|
|
333
334
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
Per question every question, model by model
|
|
340
|
-
Is the difference real? whether one model is really better than another, or it's noise
|
|
341
|
-
Tests that separate the questions that tell models apart
|
|
335
|
+
PTA index parsimony (tokens), time and accuracy per model, a bar each (longer = better;
|
|
336
|
+
2.4× = 2.4 times the best), with per-answer numbers, tok/s, TTFT and memory in
|
|
337
|
+
the table. Results open here.
|
|
338
|
+
Production readiness one verdict per model and use case; the table says why and what to run next
|
|
339
|
+
Per question every question, model by model (filters: pack, level, Models disagree)
|
|
342
340
|
Failures every wrong answer with the grader's reason; enter opens it in full
|
|
343
|
-
|
|
341
|
+
|
|
342
|
+
More in the terminal: tuieval compare (--pairwise: is a difference real), tuieval history,
|
|
343
|
+
tuieval items (test quality).
|
|
344
344
|
|
|
345
345
|
[b]Keys[/b] esc back · ctrl+r refresh · w runs""",
|
|
346
346
|
"TuneScreen": """[b]Tuning speed flags[/b]
|
|
@@ -2229,7 +2229,7 @@ class RunScreen(Screen):
|
|
|
2229
2229
|
if self.running and self.total > done:
|
|
2230
2230
|
left, rough = self.eta_seconds()
|
|
2231
2231
|
eta = f" · ETA {'~' if rough else ''}{fmt_secs(left)}"
|
|
2232
|
-
pct =
|
|
2232
|
+
pct = compare.pct(passed / (passed + failed)) if passed + failed else "-"
|
|
2233
2233
|
recent = self.stats[-50:]
|
|
2234
2234
|
tps = [r["gen_tps"] for r in recent if r.get("gen_tps")]
|
|
2235
2235
|
ttfts = [r["ttft_s"] for r in recent if r.get("ttft_s") is not None and not r.get("cached_tokens")]
|
|
@@ -2347,8 +2347,8 @@ class RunScreen(Screen):
|
|
|
2347
2347
|
class ResultsScreen(Screen):
|
|
2348
2348
|
BINDINGS = [Binding("escape", "app.pop_screen", "Back"), Binding("q", "app.pop_screen", "Back", show=False),
|
|
2349
2349
|
Binding("w", "runs", "Runs"), Binding("ctrl+r", "refresh", "Refresh")]
|
|
2350
|
-
SORTS = [("
|
|
2351
|
-
("
|
|
2350
|
+
SORTS = [("By token gap", "tokens"), ("By time gap", "secs"),
|
|
2351
|
+
("By most tokens", "most"), ("By pack order", "order")]
|
|
2352
2352
|
|
|
2353
2353
|
def check_action(self, action, parameters):
|
|
2354
2354
|
return bool(self.app.sessions) if action == "runs" else True
|
|
@@ -2378,9 +2378,9 @@ class ResultsScreen(Screen):
|
|
|
2378
2378
|
with Horizontal(id="pta-controls"):
|
|
2379
2379
|
yield Select([("All packs", "")], id="pta-pack", allow_blank=False, value="")
|
|
2380
2380
|
yield Button("Models: all", id="pta-models")
|
|
2381
|
-
yield Static("[dim]P parsimony: tokens used
|
|
2382
|
-
"answers right.
|
|
2383
|
-
"
|
|
2381
|
+
yield Static("[dim]P parsimony (tokens): tokens used, reasoning and answer. T time: total time. "
|
|
2382
|
+
"A accuracy: answers right. Verdicts and critical failures are in Production "
|
|
2383
|
+
"readiness.[/dim]")
|
|
2384
2384
|
yield Static(id="pta-note")
|
|
2385
2385
|
yield Static(id="pta-bars")
|
|
2386
2386
|
yield DataTable(id="pta", zebra_stripes=True, cursor_type="none")
|
|
@@ -2393,22 +2393,6 @@ class ResultsScreen(Screen):
|
|
|
2393
2393
|
"measured there, or projected from token counts and that machine's tuned speeds.[/dim]")
|
|
2394
2394
|
yield DataTable(id="latency", zebra_stripes=True)
|
|
2395
2395
|
yield DataTable(id="readiness-detail", zebra_stripes=True, cursor_type="row")
|
|
2396
|
-
with TabPane("Verdict history"):
|
|
2397
|
-
yield Static("[dim]Every change in a model's verdict, newest first. Kept even after results are rerun "
|
|
2398
|
-
"or questions change; the evidence behind older verdicts is in results/<model>/history/.[/dim]")
|
|
2399
|
-
yield DataTable(id="history", zebra_stripes=True, cursor_type="row")
|
|
2400
|
-
with TabPane("Scorecard"):
|
|
2401
|
-
yield DataTable(id="scorecard", zebra_stripes=True)
|
|
2402
|
-
with TabPane("Speed & tokens"):
|
|
2403
|
-
yield Select([("As measured (all machines)", "")] +
|
|
2404
|
-
[(f"{mc.id}{' (this machine)' if i == 0 else ''}", mc.id)
|
|
2405
|
-
for i, mc in enumerate(self.app.engine.machines())],
|
|
2406
|
-
id="speed-machine", allow_blank=False, value="")
|
|
2407
|
-
yield Static("[dim]★ = no other model is both more accurate and faster per answer. "
|
|
2408
|
-
"Memory is the server's resident size (includes GPU-mapped weights on Apple Silicon). "
|
|
2409
|
-
"On another machine, times are measured there if the model ran there, otherwise "
|
|
2410
|
-
"projected from each answer's token counts and that machine's tuned speeds.[/dim]")
|
|
2411
|
-
yield DataTable(id="speed", zebra_stripes=True)
|
|
2412
2396
|
with TabPane("Per question", id="tab-per-question"):
|
|
2413
2397
|
with Horizontal(id="pq-controls"):
|
|
2414
2398
|
yield Select([("All packs", "")], id="pq-pack", allow_blank=False, value="")
|
|
@@ -2416,28 +2400,16 @@ class ResultsScreen(Screen):
|
|
|
2416
2400
|
id="pq-level", allow_blank=False, value="")
|
|
2417
2401
|
yield Select(self.SORTS, id="pq-sort", allow_blank=False, value="tokens")
|
|
2418
2402
|
yield Button("Models: all", id="pq-models")
|
|
2419
|
-
yield Checkbox("
|
|
2420
|
-
yield Checkbox("
|
|
2403
|
+
yield Checkbox("Answered by all", False, id="pq-shared")
|
|
2404
|
+
yield Checkbox("Models disagree", False, id="pq-disagree")
|
|
2405
|
+
yield Checkbox("In progress", True, id="pq-partial")
|
|
2421
2406
|
yield Static(id="pq-compare-title")
|
|
2422
2407
|
yield DataTable(id="pq-summary", zebra_stripes=True, cursor_type="none")
|
|
2423
2408
|
yield Static(id="pq-legend")
|
|
2424
2409
|
yield DataTable(id="per-question", zebra_stripes=True, cursor_type="cell")
|
|
2425
|
-
with TabPane("Is the difference real?"):
|
|
2426
|
-
yield DataTable(id="pairwise", zebra_stripes=True)
|
|
2427
|
-
with TabPane("Tests that separate models"):
|
|
2428
|
-
yield DataTable(id="separating", zebra_stripes=True)
|
|
2429
|
-
with TabPane("By difficulty"):
|
|
2430
|
-
yield Static("[dim]Pass rate per difficulty level. The level is your label only; models never see it. "
|
|
2431
|
-
"A strong model should hold up on HARD, not just EASY.[/dim]")
|
|
2432
|
-
yield DataTable(id="difficulty", zebra_stripes=True)
|
|
2433
2410
|
with TabPane("Failures"):
|
|
2434
2411
|
yield Static("[dim]Enter on a row shows the whole answer, its reasoning and what was checked.[/dim]")
|
|
2435
2412
|
yield DataTable(id="failures", zebra_stripes=True, cursor_type="row")
|
|
2436
|
-
with TabPane("Test quality"):
|
|
2437
|
-
yield Static("[dim]no signal: every model passed · suspicious: every model failed (check the test) · "
|
|
2438
|
-
"inverted: the weakest model beat the strongest · flaky: same model, different results "
|
|
2439
|
-
"across repeats[/dim]")
|
|
2440
|
-
yield DataTable(id="quality", zebra_stripes=True)
|
|
2441
2413
|
yield Footer()
|
|
2442
2414
|
|
|
2443
2415
|
def on_mount(self):
|
|
@@ -2494,7 +2466,6 @@ class ResultsScreen(Screen):
|
|
|
2494
2466
|
|
|
2495
2467
|
def fill(self):
|
|
2496
2468
|
results_dir = self.results_dir or self.app.engine.results_dir
|
|
2497
|
-
self.fill_history()
|
|
2498
2469
|
paths = compare.default_paths(results_dir)
|
|
2499
2470
|
self.partial_rows = compare.load_partials(results_dir)
|
|
2500
2471
|
self.superseded = {p for p in compare.default_paths(results_dir) if self.app.engine.superseded(p)}
|
|
@@ -2511,40 +2482,13 @@ class ResultsScreen(Screen):
|
|
|
2511
2482
|
smoke = os.path.abspath(results_dir) != os.path.abspath(self.app.engine.results_dir)
|
|
2512
2483
|
info.update(f"{len(paths)} {'Smoke ' if smoke else ''}result files in {os.path.relpath(results_dir)}/" + banner
|
|
2513
2484
|
+ "".join(f"\n[yellow]⚠ {n}[/yellow]" for n in notes))
|
|
2514
|
-
header, table = compare.scorecard(rows)
|
|
2515
|
-
t = self.query_one("#scorecard", DataTable)
|
|
2516
|
-
t.add_columns(*header)
|
|
2517
|
-
t.add_rows(table)
|
|
2518
2485
|
self.rows, self.infos = rows, infos
|
|
2519
|
-
self.fill_speed(self.query_one("#speed-machine", Select).value or "")
|
|
2520
|
-
p = self.query_one("#pairwise", DataTable)
|
|
2521
|
-
p.add_columns("Model A", "Model B", "A − B", "95% CI", "Verdict", "Shared tests")
|
|
2522
|
-
for a, b, diff, lo, hi, call, n in compare.pairwise(rows):
|
|
2523
|
-
color = "green" if call == "clear" else "dim"
|
|
2524
|
-
p.add_row(a, b, f"{100 * diff:+.1f} pts", f"[{100 * lo:+.1f}, {100 * hi:+.1f}]",
|
|
2525
|
-
f"[{color}]{call}[/{color}]", str(n))
|
|
2526
|
-
split, _ = compare.separating(rows)
|
|
2527
|
-
sep = self.query_one("#separating", DataTable)
|
|
2528
|
-
models = sorted({r["model"] for r in rows})
|
|
2529
|
-
sep.add_columns("Test", *models)
|
|
2530
|
-
for test, d in split:
|
|
2531
|
-
sep.add_row(test, *[f"{sum(d[m])}/{len(d[m])}" if m in d else "-" for m in models])
|
|
2532
2486
|
f = self.query_one("#failures", DataTable)
|
|
2533
2487
|
f.add_columns("Model", "Level", "Test", "Reason")
|
|
2534
2488
|
# compare.failures lists the failed rows in order, so they line up with fail_rows
|
|
2535
2489
|
self.fail_rows = [r for r in rows if not r["ok"]]
|
|
2536
2490
|
for model, test, reason, level in compare.failures(rows):
|
|
2537
2491
|
f.add_row(model, diff_badge(level), test, rich_escape(reason[:130]))
|
|
2538
|
-
dh, dt = compare.by_difficulty(rows)
|
|
2539
|
-
dtab = self.query_one("#difficulty", DataTable)
|
|
2540
|
-
dtab.add_columns("Model", "Pack", *[diff_badge(l) for l in compare.LEVELS])
|
|
2541
|
-
for line in dt:
|
|
2542
|
-
dtab.add_row(*line)
|
|
2543
|
-
qt = self.query_one("#quality", DataTable)
|
|
2544
|
-
qt.add_columns("Flags", "Test", *models)
|
|
2545
|
-
if len(models) >= 2:
|
|
2546
|
-
for test, flags, rates in compare.items(rows):
|
|
2547
|
-
qt.add_row(", ".join(flags), test, *[f"{100 * rates[m]:.0f}%" if m in rates else "-" for m in models])
|
|
2548
2492
|
self.fill_per_question()
|
|
2549
2493
|
self.fill_pta()
|
|
2550
2494
|
self.fill_readiness(results_dir)
|
|
@@ -2565,6 +2509,7 @@ class ResultsScreen(Screen):
|
|
|
2565
2509
|
@on(Select.Changed, "#pq-sort")
|
|
2566
2510
|
@on(Checkbox.Changed, "#pq-partial")
|
|
2567
2511
|
@on(Checkbox.Changed, "#pq-shared")
|
|
2512
|
+
@on(Checkbox.Changed, "#pq-disagree")
|
|
2568
2513
|
def per_question_changed(self):
|
|
2569
2514
|
if getattr(self, "rows", None) is not None:
|
|
2570
2515
|
self.fill_per_question()
|
|
@@ -2624,7 +2569,7 @@ class ResultsScreen(Screen):
|
|
|
2624
2569
|
self.query_one("#pta-left-out", Static).update("")
|
|
2625
2570
|
return
|
|
2626
2571
|
# none picked: every model with enough answers (pta.index leaves out the rest, and says so)
|
|
2627
|
-
result = pta.index(rows, sorted(chosen) or None)
|
|
2572
|
+
result = pta.index(rows, sorted(chosen) or None, pta.memory_gb(self.infos))
|
|
2628
2573
|
self.query_one("#pta-note", Static).update(pta.scope_note(result, sorted(present)))
|
|
2629
2574
|
self.query_one("#pta-bars", Static).update("\n".join(pta.bars(result)) if result["questions"] else "")
|
|
2630
2575
|
self.query_one("#pta-left-out", Static).update(
|
|
@@ -2671,6 +2616,8 @@ class ResultsScreen(Screen):
|
|
|
2671
2616
|
shown = sorted(chosen) if chosen else sorted({m for t in tests for m in t["cells"]})
|
|
2672
2617
|
if self.query_one("#pq-shared", Checkbox).value:
|
|
2673
2618
|
tests = [t for t in tests if all(m in t["cells"] for m in shown)]
|
|
2619
|
+
if self.query_one("#pq-disagree", Checkbox).value: # some models got it right, others didn't
|
|
2620
|
+
tests = [t for t in tests if len({c["passed"] == c["n"] for c in t["cells"].values()}) > 1]
|
|
2674
2621
|
self.fill_pq_summary(tests, shown)
|
|
2675
2622
|
order = {}
|
|
2676
2623
|
for name, pk in self.app.engine.packs.items():
|
|
@@ -2801,38 +2748,6 @@ class ResultsScreen(Screen):
|
|
|
2801
2748
|
return
|
|
2802
2749
|
self.app.push_screen(AnswerScreen(entries, what="repeat"))
|
|
2803
2750
|
|
|
2804
|
-
@on(Select.Changed, "#speed-machine")
|
|
2805
|
-
def machine_changed(self, event):
|
|
2806
|
-
if getattr(self, "rows", None) is not None:
|
|
2807
|
-
self.fill_speed(event.value or "")
|
|
2808
|
-
|
|
2809
|
-
def fill_speed(self, machine_id):
|
|
2810
|
-
e = self.app.engine
|
|
2811
|
-
speeds = {}
|
|
2812
|
-
if machine_id:
|
|
2813
|
-
mc = next(x for x in e.machines() if x.id == machine_id)
|
|
2814
|
-
speeds = {m["label"]: (e.serving(m, mc).profile or {}).get("measured") for m in e.cfg["models"]}
|
|
2815
|
-
header, table, _ = compare.speed(self.rows, self.infos, machine_id or None, speeds)
|
|
2816
|
-
sp = self.query_one("#speed", DataTable)
|
|
2817
|
-
sp.clear(columns=True)
|
|
2818
|
-
sp.add_columns(*header)
|
|
2819
|
-
for line in table:
|
|
2820
|
-
sp.add_row(*([f"[yellow]{line[0]}[/yellow]"] + line[1:]))
|
|
2821
|
-
|
|
2822
|
-
def fill_history(self):
|
|
2823
|
-
e = self.app.engine
|
|
2824
|
-
t = self.query_one("#history", DataTable)
|
|
2825
|
-
t.add_columns("When", "Model", "Use case", "Change", "Questions", "Why")
|
|
2826
|
-
rank = {"FAIL": 0, "INCONCLUSIVE": 1, "NO DATA": 1, "PASS": 2}
|
|
2827
|
-
for h in reversed(verdict.load_history(e)):
|
|
2828
|
-
prev, now = h.get("previous"), h["status"]
|
|
2829
|
-
change = f"[{VERDICT_STYLE[now]}]{now}[/]" if not prev else \
|
|
2830
|
-
f"{prev} → [{VERDICT_STYLE[now]}]{now}[/]"
|
|
2831
|
-
if prev and rank.get(now, 1) < rank.get(prev, 1):
|
|
2832
|
-
change += " [bold red]⚠ worse[/bold red]"
|
|
2833
|
-
qs = "current" if verdict.is_current(h, e) else "[yellow]changed since[/yellow]"
|
|
2834
|
-
t.add_row(h["time"].replace("T", " "), h["model"], h["use_case"], change, qs, "; ".join(h["reasons"])[:120])
|
|
2835
|
-
|
|
2836
2751
|
def fill_readiness(self, results_dir):
|
|
2837
2752
|
style = {"PASS": "bold green", "FAIL": "bold red", "INCONCLUSIVE": "yellow", "NO DATA": "dim"}
|
|
2838
2753
|
matrix = self.query_one("#readiness", DataTable)
|
|
@@ -2844,17 +2759,18 @@ class ResultsScreen(Screen):
|
|
|
2844
2759
|
return
|
|
2845
2760
|
help_text.update(self.READINESS_HELP)
|
|
2846
2761
|
table = verdict.readiness(self.app.engine)
|
|
2847
|
-
short = ["[b]In short[/b]"]
|
|
2762
|
+
short = ["[b]In short[/b]"] # one line per model; what to run next is in the table below
|
|
2848
2763
|
for label, sentence, todo in verdict.plain_summary(table):
|
|
2849
2764
|
short.append(f" [b]{rich_escape(label)}[/b]: {rich_escape(sentence)}")
|
|
2850
|
-
short += [f" → {rich_escape(g)}: {rich_escape(what)}" for g, what, _ in todo]
|
|
2851
2765
|
if any("Certify" in what for _, _, todo in verdict.plain_summary(table) for _, what, _ in todo):
|
|
2852
2766
|
short.append(" [dim]To run Certify: in Setup tick the model and pack, choose Certify, press s.[/dim]")
|
|
2853
2767
|
self.query_one("#readiness-short", Static).update("\n".join(short) if table else
|
|
2854
2768
|
"[dim]No verdicts yet: run something from Setup.[/dim]")
|
|
2855
2769
|
groups = sorted({g for t in table.values() for g in t})
|
|
2856
2770
|
matrix.add_columns("Model", *groups)
|
|
2857
|
-
detail.add_columns("Model", "Use case", "Pack", "Verdict", "Why")
|
|
2771
|
+
detail.add_columns("Model", "Use case", "Pack", "Verdict", "Answers right", "Why")
|
|
2772
|
+
right = collections.Counter((r["model"], r["suite"]) for r in self.rows if r["ok"])
|
|
2773
|
+
answered = collections.Counter((r["model"], r["suite"]) for r in self.rows)
|
|
2858
2774
|
lat = verdict.latency_table(self.app.engine, None, table)
|
|
2859
2775
|
machine_ids = [mc.id for mc in self.app.engine.machines()]
|
|
2860
2776
|
lt = self.query_one("#latency", DataTable)
|
|
@@ -2871,7 +2787,9 @@ class ResultsScreen(Screen):
|
|
|
2871
2787
|
for g, (_, packs) in gs.items():
|
|
2872
2788
|
for name, pv in packs.items():
|
|
2873
2789
|
if pv.status != "NO DATA":
|
|
2874
|
-
|
|
2790
|
+
n = answered[(label, name)]
|
|
2791
|
+
detail.add_row(label, g, name, f"[{style[pv.status]}]{pv.status}[/]",
|
|
2792
|
+
f"{right[(label, name)]}/{n}" if n else "-", "; ".join(pv.reasons)[:160])
|
|
2875
2793
|
|
|
2876
2794
|
|
|
2877
2795
|
# ------------------------------------------------------------------ app
|
|
@@ -2932,7 +2850,6 @@ class EvalsApp(App):
|
|
|
2932
2850
|
#machine { padding: 0 1; height: auto; }
|
|
2933
2851
|
#tune-status { padding: 0 1; height: auto; min-height: 2; background: $boost; }
|
|
2934
2852
|
#tune-log { height: 1fr; }
|
|
2935
|
-
#speed-machine { width: 60; margin: 0 1; }
|
|
2936
2853
|
#results-top { height: auto; }
|
|
2937
2854
|
#results-info { width: 1fr; height: auto; }
|
|
2938
2855
|
#toggle-smoke { width: auto; min-width: 32; }
|
|
@@ -2943,7 +2860,7 @@ class EvalsApp(App):
|
|
|
2943
2860
|
#pta { width: 1fr; height: auto; margin-top: 1; }
|
|
2944
2861
|
#pta-left-out { height: auto; padding: 1 0 0 0; }
|
|
2945
2862
|
#pq-controls Checkbox { width: auto; }
|
|
2946
|
-
#pq-controls Select { width:
|
|
2863
|
+
#pq-controls Select { width: 22; margin-right: 1; }
|
|
2947
2864
|
#pq-models { margin-right: 1; min-width: 20; }
|
|
2948
2865
|
#pq-compare-title { padding: 0 1; height: auto; }
|
|
2949
2866
|
#pq-summary { height: auto; max-height: 10; }
|
tuieval/tune.py
CHANGED
|
@@ -60,6 +60,7 @@ SWAP_NOTABLE = 256 * 2**20 # swap worth a warning (bytes); rejecting for swap n
|
|
|
60
60
|
ANSWER_TOKENS = 1024 # answer length the projected objective scores ([tune] answer_tokens)
|
|
61
61
|
PRESSURE_CRITICAL = 4 # macOS memory pressure level that rejects a candidate
|
|
62
62
|
PRESSURE_EVERY_S = 2
|
|
63
|
+
VRAM_MARGIN_GB = 0.25 # a discrete GPU this close to full could run out on a longer prompt
|
|
63
64
|
GPU_MARGIN_GB = 0.75 # a candidate whose GPU allocation comes this close to the residency limit
|
|
64
65
|
# is rejected even before it stalls: servers grow as requests arrive
|
|
65
66
|
REQUEST_LIMIT_S = 600 # a tuning request taking longer fails the candidate ([tune] request_timeout_s)
|
|
@@ -556,6 +557,14 @@ def tune(eng, label, emit=lambda *a, **k: None, max_starts=16, min_gain=0.03, us
|
|
|
556
557
|
r.facts["gpu_settled_gb"] = round(settled, 1)
|
|
557
558
|
if info.get("kernel_share_max") is not None:
|
|
558
559
|
r.facts["kernel_share"] = info["kernel_share_max"]
|
|
560
|
+
mc = eng.machine()
|
|
561
|
+
if mc.discrete: # VRAM is the limit there: its own memory, nothing else can use it
|
|
562
|
+
vram = machines.vram_used_gb()
|
|
563
|
+
if vram is not None:
|
|
564
|
+
r.facts["vram_used_gb"] = round(vram, 1)
|
|
565
|
+
if vram > mc.gpu_limit_gb - VRAM_MARGIN_GB and not r.error:
|
|
566
|
+
r.error = (f"VRAM nearly full: {vram:.1f} of {mc.gpu_limit_gb:.0f} GB in use "
|
|
567
|
+
"(a longer prompt could run out)")
|
|
559
568
|
limit = eng.gpu_residency_gb()
|
|
560
569
|
if settled and settled > limit - GPU_MARGIN_GB and not r.error:
|
|
561
570
|
r.error = (f"GPU allocation settled at {settled:.1f} GB, within {GPU_MARGIN_GB} GB of the "
|
tuieval/verdict.py
CHANGED
|
@@ -259,7 +259,8 @@ def _plain_fail(v):
|
|
|
259
259
|
if "cut off" in text:
|
|
260
260
|
return "too many answers cut off by max_tokens"
|
|
261
261
|
if "accuracy" in text and ev.get("trials"):
|
|
262
|
-
|
|
262
|
+
from .compare import pct # local: compare imports nothing from here, but keep verdict light
|
|
263
|
+
return f"{pct(ev['accuracy'])} right, under the bar"
|
|
263
264
|
return text.split(" (")[0]
|
|
264
265
|
|
|
265
266
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev65
|
|
4
4
|
Summary: Evaluate local and frontier LLMs on your own questions: accuracy, speed, tokens and PASS/FAIL verdicts, in the terminal.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ashe-wb/tuieval
|
|
6
6
|
Project-URL: Issues, https://github.com/ashe-wb/tuieval/issues
|
|
@@ -51,7 +51,7 @@ Whatever runs your models, the first steps are short:
|
|
|
51
51
|
| You have | Do this |
|
|
52
52
|
|---|---|
|
|
53
53
|
| **LM Studio, Ollama or vLLM** | Start its server and load a model, then `tuieval init my-evals`: it finds the model. Later: `tuieval add` lists new ones. |
|
|
54
|
-
| **GGUF files** | Install llama.cpp (`brew install llama.cpp
|
|
54
|
+
| **GGUF files** | Install llama.cpp (`brew install llama.cpp`, or a CUDA/ROCm build on Linux), then `tuieval add ~/models/Some-Model-Q4_K_M.gguf`. tuieval starts `llama-server` for it, sized to your Mac's memory or your Nvidia/AMD card's VRAM. |
|
|
55
55
|
| **An OpenRouter key** | `export OPENROUTER_API_KEY=…`, then `tuieval run --tier smoke --only openrouter:<model id>`. |
|
|
56
56
|
|
|
57
57
|
Stuck? `tuieval doctor` checks servers, models, packs and keys, and says what to fix.
|
|
@@ -76,7 +76,7 @@ Afterwards, every answer keeps its reasoning. Enter on a result opens it with th
|
|
|
76
76
|
- **Verdicts you can act on.** A pack passes only with zero critical failures over enough trials, and an accuracy whose 95% lower bound clears your bar. *INCONCLUSIVE* says what evidence is missing.
|
|
77
77
|
- **Two tiers:** *Screen* a sample of every pack to drop weak models fast, then *Certify* finalists on every test with repeats. Certification reuses the screening answers.
|
|
78
78
|
- **Honest numbers:** every request is a fresh single-turn conversation, with prompt caching off and repeat rounds in different orders. Each answer records what was sent, and the run warns about reused prompts or identical repeats.
|
|
79
|
-
- **Per-machine speed.** A fit check picks the largest context that fits your Mac's GPU memory, `tuieval tune` finds the fastest speed-only server flags (with a guard that rejects flags that change answers), and readiness includes a *Fast enough?* table per machine, measured or projected.
|
|
79
|
+
- **Per-machine speed.** A fit check picks the largest context that fits your Mac's GPU memory or your Nvidia/AMD card's VRAM, `tuieval tune` finds the fastest speed-only server flags (with a guard that rejects flags that change answers), and readiness includes a *Fast enough?* table per machine, measured or projected.
|
|
80
80
|
- **Tests that check themselves.** Every test carries a reference answer and known-wrong answers, and `tuieval selftest` checks the grader accepts the first and rejects the second, and that each gate is reachable at all.
|
|
81
81
|
- **History.** Verdict changes are appended to `results/verdicts.jsonl`, and replaced results are moved to `history/`, never overwritten.
|
|
82
82
|
|
|
@@ -144,7 +144,7 @@ See **[docs/models.md](docs/models.md)** for servers, OpenRouter endpoint pinnin
|
|
|
144
144
|
1. **Pick packs and models** (space ticks; type to filter models by label or tag). Each model shows a short code per use case, e.g. `C✓ S?` (✓ pass, ✗ fail, ? inconclusive; grey = from earlier results). To run only some tests of a pack, highlight it and press `e`. Runs of different tests add up, and the pack can PASS once all its tests have run.
|
|
145
145
|
2. **Pick a tier** (Smoke to check setup, Screen, Certify) and **press `s`**. The line above the buttons shows how many answers that is and roughly how long it will take. For each model, tuieval starts its server (or uses a running one), checks the right model is loaded, runs every selected pack, then stops it.
|
|
146
146
|
3. **Watch the run:** progress with ETA, live score, tok/s, TTFT and memory, the current test with reasoning and answer side by side, recent results with the grader's reason. `k` skips a model, `c` cancels (finished work is kept and resumes next time). `n` sets up more evals while it runs: add them to this run (same tier) or queue them as the next run.
|
|
147
|
-
4. **Press `r` for results:**
|
|
147
|
+
4. **Press `r` for results:** the PTA index (parsimony (tokens), time and accuracy as bars), production readiness, per question, and failures. `tuieval compare`, `tuieval history` and `tuieval items` have the rest: whether a difference is real, verdict history and test quality.
|
|
148
148
|
|
|
149
149
|
Other keys: `t` tunes the ticked models' speed flags, `a` adds a model, `m` scans for GGUFs, `p` saves or loads a preset, `x` hides a model. `?` explains any screen.
|
|
150
150
|
|
|
@@ -176,7 +176,7 @@ tuieval help
|
|
|
176
176
|
|
|
177
177
|
- **Start with Production readiness.** A single critical failure already means FAIL, whatever the accuracy: a model that breaks a hard rule once in 300 answers will do it in production. The Failures tab and `tuieval report` list exactly which answers failed.
|
|
178
178
|
- **INCONCLUSIVE is not "nearly passed".** It says what's missing (usually a Certify run, or more trials).
|
|
179
|
-
- **Trust
|
|
179
|
+
- **Trust `tuieval compare --pairwise` over raw percentages.** Differences of one or two tests are usually noise.
|
|
180
180
|
- **Read `trunc` before accuracy.** A model that runs out of tokens isn't wrong; it's thinking too long for the budget.
|
|
181
181
|
- **Watch TTFT for interactive use.** A model that is 5% more accurate but takes 3 s longer to start answering may be the worse choice.
|
|
182
182
|
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
tuieval/__init__.py,sha256=myVXVX7htbwezW6k3x_FuKgn1RSZ2WcTaDEMByGn6Wg,312
|
|
2
2
|
tuieval/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
|
|
3
|
-
tuieval/_version.py,sha256=
|
|
3
|
+
tuieval/_version.py,sha256=QkealilRB02_mYePp9PrQfXW0yGA1qHLQzzPeETGVN4,535
|
|
4
4
|
tuieval/builtin.toml,sha256=Kxw22RZCJhURUyu5Wz8tj5Y1nM85A8hFVRbA4ocoPUg,4293
|
|
5
|
-
tuieval/cli.py,sha256=
|
|
5
|
+
tuieval/cli.py,sha256=fn6z0cupM-aW3fY5fFgnS-HmOQmMzlDg-U76Y6LgL3o,4510
|
|
6
6
|
tuieval/client.py,sha256=kpsyLEZKfxCsvIGBHBbBy83TpI97CvnGCDq-4AbJS4Y,9986
|
|
7
|
-
tuieval/compare.py,sha256=
|
|
8
|
-
tuieval/doctor.py,sha256=
|
|
9
|
-
tuieval/engine.py,sha256=
|
|
7
|
+
tuieval/compare.py,sha256=T6a_PC7z-jHEvw09KEceo2dxoLMi-9cHYPQs4L6Ctig,26997
|
|
8
|
+
tuieval/doctor.py,sha256=j2MdDORstsAGPnRmpsnxau1_fR6sTc_oEFGM5zugTJU,7246
|
|
9
|
+
tuieval/engine.py,sha256=SsXqR1ydn827qBX8NoawyT2roNuelmrZB-hOuyGSesY,116514
|
|
10
10
|
tuieval/export.py,sha256=dbHQgL_ubWtIPM1sLzAew_gETnrDJyd0zgqVA5vkpXM,9657
|
|
11
|
-
tuieval/machines.py,sha256=
|
|
11
|
+
tuieval/machines.py,sha256=t9tu6CZeXZON4EtOVnEwP4VtaynpVLf9Cz8TNjf7jtY,20124
|
|
12
12
|
tuieval/onboard.py,sha256=IYETVDWCCo9vF0Dtj4_rr9VanYeCky8PwP3-2g5A_l0,6215
|
|
13
13
|
tuieval/packs.py,sha256=tiLmxsrX_M5EUMuR4Yvb7pLBLEI15wamHSjflD6wjoY,11299
|
|
14
14
|
tuieval/profiles.py,sha256=soeLxjdB-l-OxSY8jZ4yjwfdSnsBV-QICcwKxRJS7nQ,3663
|
|
15
|
-
tuieval/pta.py,sha256=
|
|
15
|
+
tuieval/pta.py,sha256=6HwPMvTTJFoZjO-2ZurucJrp1itIzpDTKL1xvYP9W0w,10888
|
|
16
16
|
tuieval/remove.py,sha256=Vwh43nm9lVb9U9aN-h6CMYbkTwdR3UWfZObwweF72n0,5157
|
|
17
|
-
tuieval/run_evals.py,sha256=
|
|
17
|
+
tuieval/run_evals.py,sha256=1zNkCdFt744cqpebqiyj-sZrbOI22c6IYBcXk9koz20,46538
|
|
18
18
|
tuieval/scaffold.py,sha256=YBOqniJpf_GtKKAFF3_OHxxMGbmTWdFjcClGcOnrrKE,12130
|
|
19
19
|
tuieval/selftest.py,sha256=nExkrOidgymlmvkxImEBfrQG5iTUN5WuRMIKtqusCCs,9835
|
|
20
|
-
tuieval/tui.py,sha256=
|
|
21
|
-
tuieval/tune.py,sha256=
|
|
22
|
-
tuieval/verdict.py,sha256=
|
|
20
|
+
tuieval/tui.py,sha256=lrIv4z21s9OG809706Z-lE2-TZ6gg8z0AjaJ_zW7zkw,161021
|
|
21
|
+
tuieval/tune.py,sha256=azxxthgJeag8JyoQUgaRxq6oI1XvEgzb9vkszjJZH7w,39645
|
|
22
|
+
tuieval/verdict.py,sha256=VB-0LTxR9y8bJHT9RicxtE9K7cUHBdCu40hOCrsbmA4,19150
|
|
23
23
|
tuieval/watch_proxy.py,sha256=bnDO1i6n6yUs1hODoUWcMppJMGPavoQqs6aNYrIxpT8,11416
|
|
24
24
|
tuieval/workspace.py,sha256=V_HhkaN1odr0OpjNsPYradRWLpzDldM7SwpvxohBL4U,895
|
|
25
25
|
tuieval/yamlout.py,sha256=r1IokjLqW-Ztpg1fSn33ZW3VK00v1VvxymmU5Qy7GEw,606
|
|
@@ -46,8 +46,8 @@ tuieval/templates/packs/tool_call/pack.toml,sha256=n4bx0lGJHSmW41uVqhfIXJ2BWZpmT
|
|
|
46
46
|
tuieval/templates/packs/tool_call/system.txt,sha256=mKxu-lB-0YjLHXdgeFJa8x2C180OvcNz5cHv2N5Bg3U,254
|
|
47
47
|
tuieval/templates/packs/tool_call/tests.yaml,sha256=Nv7c4syrzJ9cU8EBKSK_1ZSFx9V02aCjW4-2TBCPT7s,1859
|
|
48
48
|
tuieval/templates/packs/tool_call/tools.yaml,sha256=HgxCkh0On2VwbHSsjAGM8tNCK7ZtKLGrs7lMCn3Fprs,1048
|
|
49
|
-
tuieval-0.2.0.
|
|
50
|
-
tuieval-0.2.0.
|
|
51
|
-
tuieval-0.2.0.
|
|
52
|
-
tuieval-0.2.0.
|
|
53
|
-
tuieval-0.2.0.
|
|
49
|
+
tuieval-0.2.0.dev65.dist-info/METADATA,sha256=Wd-olyVQVqZyQTb817isEBh7GUdLGbBQDxy2Ioc7iBw,13996
|
|
50
|
+
tuieval-0.2.0.dev65.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
51
|
+
tuieval-0.2.0.dev65.dist-info/entry_points.txt,sha256=7eIhi1GpFxBP2OLJV02Nn8LwsNuO512eKOGt1IBbj04,45
|
|
52
|
+
tuieval-0.2.0.dev65.dist-info/licenses/LICENSE,sha256=7wfRgGtBGH1aXhZkIn47oUznqKC18kEPMvUVgol7Ffc,1077
|
|
53
|
+
tuieval-0.2.0.dev65.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|