benchkit-cli 0.1.0__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.
- benchkit/__init__.py +1 -0
- benchkit/_pi_proxy.py +219 -0
- benchkit/answer_key_guard.ts +49 -0
- benchkit/artifacts.py +87 -0
- benchkit/benchmarks/__init__.py +105 -0
- benchkit/benchmarks/aider_polyglot.py +405 -0
- benchkit/benchmarks/arc.py +52 -0
- benchkit/benchmarks/banking77.py +149 -0
- benchkit/benchmarks/base.py +19 -0
- benchkit/benchmarks/boolq.py +49 -0
- benchkit/benchmarks/chartqa.py +133 -0
- benchkit/benchmarks/code_verifier.py +77 -0
- benchkit/benchmarks/downloads.py +166 -0
- benchkit/benchmarks/evalplus.py +288 -0
- benchkit/benchmarks/git_surgery.py +485 -0
- benchkit/benchmarks/gpqa.py +56 -0
- benchkit/benchmarks/gsm8k.py +97 -0
- benchkit/benchmarks/hellaswag.py +49 -0
- benchkit/benchmarks/humaneval.py +103 -0
- benchkit/benchmarks/ifeval.py +56 -0
- benchkit/benchmarks/ifeval_instructions.py +477 -0
- benchkit/benchmarks/mbpp.py +60 -0
- benchkit/benchmarks/mc_arena.py +479 -0
- benchkit/benchmarks/mcq.py +94 -0
- benchkit/benchmarks/medfailbench.py +537 -0
- benchkit/benchmarks/mmlu.py +54 -0
- benchkit/benchmarks/mmlu_pro.py +62 -0
- benchkit/benchmarks/mmvp.py +115 -0
- benchkit/benchmarks/ocrbench.py +96 -0
- benchkit/benchmarks/openbookqa.py +52 -0
- benchkit/benchmarks/patcheval.py +929 -0
- benchkit/benchmarks/piqa.py +52 -0
- benchkit/benchmarks/ruler.py +172 -0
- benchkit/benchmarks/ruler_generator.py +535 -0
- benchkit/benchmarks/sanity.py +174 -0
- benchkit/benchmarks/treejs_arena.py +268 -0
- benchkit/benchmarks/truthfulqa.py +52 -0
- benchkit/benchmarks/utils.py +25 -0
- benchkit/benchmarks/vision.py +230 -0
- benchkit/benchmarks/vstar.py +108 -0
- benchkit/benchmarks/winogrande.py +52 -0
- benchkit/benchmarks/xstest.py +116 -0
- benchkit/browser.py +429 -0
- benchkit/catalog.py +121 -0
- benchkit/cli.py +1000 -0
- benchkit/client.py +1413 -0
- benchkit/controls.py +87 -0
- benchkit/datasets/MMLU_LICENSE.txt +21 -0
- benchkit/datasets/README.md +142 -0
- benchkit/datasets/arc_challenge.jsonl +1172 -0
- benchkit/datasets/banking77.jsonl +3080 -0
- benchkit/datasets/banking77_intents.json +79 -0
- benchkit/datasets/boolq.jsonl +3270 -0
- benchkit/datasets/code_verifier.jsonl +500 -0
- benchkit/datasets/gpqa_diamond.jsonl +198 -0
- benchkit/datasets/gsm8k.jsonl +1319 -0
- benchkit/datasets/hellaswag.jsonl +1000 -0
- benchkit/datasets/humaneval.jsonl +164 -0
- benchkit/datasets/ifeval.jsonl +541 -0
- benchkit/datasets/mbpp.jsonl +500 -0
- benchkit/datasets/mc_blocks_1_20_1.jsonl +1003 -0
- benchkit/datasets/medfailbench_hard30.jsonl +30 -0
- benchkit/datasets/medfailbench_hard30.md +13 -0
- benchkit/datasets/openbookqa.jsonl +500 -0
- benchkit/datasets/piqa.jsonl +1838 -0
- benchkit/datasets/sanity_code.jsonl +6 -0
- benchkit/datasets/truthfulqa.jsonl +817 -0
- benchkit/datasets/winogrande.jsonl +1267 -0
- benchkit/datasets/xstest.csv +451 -0
- benchkit/decision.py +194 -0
- benchkit/demo.py +451 -0
- benchkit/dockerfiles.py +247 -0
- benchkit/engine.py +1270 -0
- benchkit/evaluation.py +195 -0
- benchkit/events.py +141 -0
- benchkit/executor.py +96 -0
- benchkit/git_surgery/backport-release-stack/setup.sh +328 -0
- benchkit/git_surgery/backport-release-stack/verify.sh +119 -0
- benchkit/git_surgery/bisect-the-regression/setup.sh +49 -0
- benchkit/git_surgery/bisect-the-regression/verify.sh +34 -0
- benchkit/git_surgery/rebase-conflict-chain/setup.sh +65 -0
- benchkit/git_surgery/rebase-conflict-chain/verify.sh +43 -0
- benchkit/git_surgery/recover-complex-stash/setup.sh +152 -0
- benchkit/git_surgery/recover-complex-stash/verify.sh +95 -0
- benchkit/git_surgery/recover-lost-work/setup.sh +40 -0
- benchkit/git_surgery/recover-lost-work/verify.sh +36 -0
- benchkit/git_surgery/repair-force-pushed-remote/setup.sh +171 -0
- benchkit/git_surgery/repair-force-pushed-remote/verify.sh +83 -0
- benchkit/git_surgery/revert-merge-with-followups/setup.sh +198 -0
- benchkit/git_surgery/revert-merge-with-followups/verify.sh +98 -0
- benchkit/git_surgery/secret-in-history/README.md +27 -0
- benchkit/git_surgery/secret-in-history/setup.sh +107 -0
- benchkit/git_surgery/secret-in-history/verify.sh +106 -0
- benchkit/git_surgery/split-the-mega-commit/setup.sh +50 -0
- benchkit/git_surgery/split-the-mega-commit/verify.sh +48 -0
- benchkit/git_surgery/untangle-nested-submodules/setup.sh +193 -0
- benchkit/git_surgery/untangle-nested-submodules/verify.sh +90 -0
- benchkit/history.py +474 -0
- benchkit/jobs.py +332 -0
- benchkit/leakage.py +51 -0
- benchkit/looping.py +873 -0
- benchkit/mc_render.py +274 -0
- benchkit/mc_viewer/dist/atlas.png +0 -0
- benchkit/mc_viewer/dist/blockStates.json.gz +0 -0
- benchkit/mc_viewer/dist/version.json +4 -0
- benchkit/mc_viewer/dist/viewer.html +23 -0
- benchkit/mc_viewer/dist/viewer.js +2 -0
- benchkit/mc_viewer/dist/viewer.js.LICENSE.txt +25 -0
- benchkit/mc_viewer/dist/worker.js +2 -0
- benchkit/mc_viewer/dist/worker.js.LICENSE.txt +19 -0
- benchkit/metrics.py +100 -0
- benchkit/ollama.py +60 -0
- benchkit/perf.py +353 -0
- benchkit/perf_report.py +207 -0
- benchkit/perturbations.py +208 -0
- benchkit/pi_agent.py +476 -0
- benchkit/pi_package/package-lock.json +1827 -0
- benchkit/pi_package/package.json +8 -0
- benchkit/report.py +142 -0
- benchkit/report_markdown.py +645 -0
- benchkit/results.py +688 -0
- benchkit/retry.py +163 -0
- benchkit/runner.py +769 -0
- benchkit/sandbox.py +1167 -0
- benchkit/template_check.py +291 -0
- benchkit/templates/arena.html +504 -0
- benchkit/templates/history.html +58 -0
- benchkit/templates/perf.html +509 -0
- benchkit/templates/report.html +769 -0
- benchkit/tui/__init__.py +5 -0
- benchkit/tui/app.py +87 -0
- benchkit/tui/app.tcss +512 -0
- benchkit/tui/formatting.py +84 -0
- benchkit/tui/screens/__init__.py +1 -0
- benchkit/tui/screens/connect.py +191 -0
- benchkit/tui/screens/detail.py +288 -0
- benchkit/tui/screens/help.py +84 -0
- benchkit/tui/screens/modals.py +544 -0
- benchkit/tui/screens/results.py +361 -0
- benchkit/tui/screens/run.py +988 -0
- benchkit/tui/screens/setup.py +733 -0
- benchkit/tui/theme.py +135 -0
- benchkit/tui/widgets.py +120 -0
- benchkit_cli-0.1.0.dist-info/METADATA +294 -0
- benchkit_cli-0.1.0.dist-info/RECORD +149 -0
- benchkit_cli-0.1.0.dist-info/WHEEL +5 -0
- benchkit_cli-0.1.0.dist-info/entry_points.txt +3 -0
- benchkit_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- benchkit_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
"""Setup screen: choose models, benchmarks and task slices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
from textual import work
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
from textual.content import Content
|
|
11
|
+
from textual.screen import Screen
|
|
12
|
+
from textual.widgets import (
|
|
13
|
+
Button,
|
|
14
|
+
Checkbox,
|
|
15
|
+
Footer,
|
|
16
|
+
Header,
|
|
17
|
+
Input,
|
|
18
|
+
Label,
|
|
19
|
+
Select,
|
|
20
|
+
SelectionList,
|
|
21
|
+
Static,
|
|
22
|
+
)
|
|
23
|
+
from textual.widgets.selection_list import Selection
|
|
24
|
+
|
|
25
|
+
from benchkit import catalog
|
|
26
|
+
from benchkit.benchmarks import DESCRIPTIONS, REGISTRY
|
|
27
|
+
from benchkit.demo import DemoClient
|
|
28
|
+
from benchkit.engine import (
|
|
29
|
+
MAX_REPAIR_ATTEMPTS,
|
|
30
|
+
JobSpec,
|
|
31
|
+
SliceError,
|
|
32
|
+
expand_jobs,
|
|
33
|
+
parse_slice,
|
|
34
|
+
route_decision_jobs,
|
|
35
|
+
slice_label,
|
|
36
|
+
slice_task_count,
|
|
37
|
+
task_count,
|
|
38
|
+
)
|
|
39
|
+
from benchkit.jobs import job_matrix
|
|
40
|
+
from benchkit.perturbations import CHOICE_ORDER, unsupported_benchmarks
|
|
41
|
+
from benchkit.template_check import TemplateReport, check_template
|
|
42
|
+
from benchkit.tui.formatting import fmt_count, fmt_size
|
|
43
|
+
from benchkit.tui.screens.modals import (
|
|
44
|
+
DatasetsScreen,
|
|
45
|
+
LimitScreen,
|
|
46
|
+
TemplateCheckScreen,
|
|
47
|
+
)
|
|
48
|
+
from benchkit.tui.theme import score_palette
|
|
49
|
+
from benchkit.tui.widgets import SectionTitle
|
|
50
|
+
|
|
51
|
+
MODEL_NAME_WIDTH = 40
|
|
52
|
+
BENCHMARK_NAME_WIDTH = 15
|
|
53
|
+
BENCHMARK_COUNT_WIDTH = 13
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SetupScreen(Screen[None]):
|
|
57
|
+
"""Pick what to run. Everything here is keyboard driven."""
|
|
58
|
+
|
|
59
|
+
BINDINGS = [
|
|
60
|
+
Binding("s", "start", "Start run"),
|
|
61
|
+
Binding("f5", "start", "Start run", show=False),
|
|
62
|
+
Binding("a", "select_all", "Select all"),
|
|
63
|
+
Binding("n", "select_none", "Clear"),
|
|
64
|
+
Binding("i", "invert", "Invert"),
|
|
65
|
+
Binding("l", "set_limit", "Task limit"),
|
|
66
|
+
Binding("p", "toggle_perturbation", "Choice order"),
|
|
67
|
+
Binding("v", "check_templates", "Check templates"),
|
|
68
|
+
Binding("d", "datasets", "Datasets"),
|
|
69
|
+
Binding("slash", "focus_filter", "Filter"),
|
|
70
|
+
Binding("escape", "back", "Back"),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
def __init__(self) -> None:
|
|
74
|
+
super().__init__()
|
|
75
|
+
self.model_order: list[str] = []
|
|
76
|
+
self.model_meta: dict[str, dict] = {}
|
|
77
|
+
self.selected_models: set[str] = set()
|
|
78
|
+
self.bench_order: list[str] = list(REGISTRY.keys())
|
|
79
|
+
self.selected_benchmarks: set[str] = set()
|
|
80
|
+
self.counts: dict[str, int] = {}
|
|
81
|
+
self.limits: dict[str, str] = {}
|
|
82
|
+
self.template_status: dict[str, str] = {}
|
|
83
|
+
self.checking_templates = False
|
|
84
|
+
self.missing_data = _missing_data()
|
|
85
|
+
|
|
86
|
+
def compose(self) -> ComposeResult:
|
|
87
|
+
yield Header(show_clock=True)
|
|
88
|
+
with Horizontal(id="setup-body"):
|
|
89
|
+
with Vertical(classes="pane", id="model-pane"):
|
|
90
|
+
yield SectionTitle("Models", id="model-title")
|
|
91
|
+
yield Input(
|
|
92
|
+
placeholder="Filter models…", id="model-filter", classes="filter"
|
|
93
|
+
)
|
|
94
|
+
yield SelectionList(id="model-list")
|
|
95
|
+
with Vertical(classes="pane", id="bench-pane"):
|
|
96
|
+
yield SectionTitle("Benchmarks", id="bench-title")
|
|
97
|
+
yield Input(
|
|
98
|
+
placeholder="Filter benchmarks…",
|
|
99
|
+
id="bench-filter",
|
|
100
|
+
classes="filter",
|
|
101
|
+
)
|
|
102
|
+
yield SelectionList(id="bench-list")
|
|
103
|
+
with Horizontal(id="setup-options"):
|
|
104
|
+
yield Label("Tasks per benchmark", classes="field-label wide")
|
|
105
|
+
yield Input(placeholder="all", id="global-limit", classes="limit-input")
|
|
106
|
+
yield Static(
|
|
107
|
+
"N first N · -N last N · A-B range · press [b]l[/b] for a per-benchmark limit",
|
|
108
|
+
classes="hint",
|
|
109
|
+
)
|
|
110
|
+
with Horizontal(id="setup-harness"):
|
|
111
|
+
yield Label("Harness", classes="field-label wide")
|
|
112
|
+
yield Select(
|
|
113
|
+
[
|
|
114
|
+
("Direct API", "direct"),
|
|
115
|
+
("Pi agent", "pi"),
|
|
116
|
+
("Direct + Pi", "both"),
|
|
117
|
+
],
|
|
118
|
+
value="direct",
|
|
119
|
+
allow_blank=False,
|
|
120
|
+
id="harness",
|
|
121
|
+
)
|
|
122
|
+
yield Input(
|
|
123
|
+
value="0",
|
|
124
|
+
type="integer",
|
|
125
|
+
max_length=2,
|
|
126
|
+
id="repair-attempts",
|
|
127
|
+
classes="repair-input",
|
|
128
|
+
)
|
|
129
|
+
yield Static(
|
|
130
|
+
f"sanitized feedback turns (0-{MAX_REPAIR_ATTEMPTS})",
|
|
131
|
+
classes="hint",
|
|
132
|
+
)
|
|
133
|
+
with Horizontal(id="setup-perturbation"):
|
|
134
|
+
yield Label("Perturbation", classes="field-label wide")
|
|
135
|
+
yield Checkbox(
|
|
136
|
+
"Run clean + choice-order",
|
|
137
|
+
id="choice-order",
|
|
138
|
+
compact=True,
|
|
139
|
+
)
|
|
140
|
+
yield Label("Seed", id="perturbation-seed-label")
|
|
141
|
+
yield Input(
|
|
142
|
+
value="42",
|
|
143
|
+
id="perturbation-seed",
|
|
144
|
+
classes="seed-input",
|
|
145
|
+
disabled=True,
|
|
146
|
+
)
|
|
147
|
+
yield Static(
|
|
148
|
+
"paired shuffled options · supported multiple-choice benchmarks only",
|
|
149
|
+
classes="hint",
|
|
150
|
+
)
|
|
151
|
+
with Horizontal(id="setup-unload"):
|
|
152
|
+
yield Label("Model unload", classes="field-label wide")
|
|
153
|
+
yield Checkbox(
|
|
154
|
+
"Force unload between models",
|
|
155
|
+
id="force-unload",
|
|
156
|
+
compact=True,
|
|
157
|
+
)
|
|
158
|
+
yield Static(
|
|
159
|
+
"evict each model from VRAM before the next model loads "
|
|
160
|
+
"(llama.cpp /models/unload)",
|
|
161
|
+
classes="hint",
|
|
162
|
+
)
|
|
163
|
+
with Horizontal(id="setup-footer"):
|
|
164
|
+
yield Static("", id="plan-summary")
|
|
165
|
+
yield Button("Datasets", id="datasets")
|
|
166
|
+
yield Button("Check templates", id="check-templates")
|
|
167
|
+
yield Button("Start run ▸", id="start", variant="primary")
|
|
168
|
+
yield Footer()
|
|
169
|
+
|
|
170
|
+
def on_mount(self) -> None:
|
|
171
|
+
self.sub_title = f"{self.app.client.label} · {self.app.client.host}"
|
|
172
|
+
self.model_order = [model["name"] for model in self.app.models]
|
|
173
|
+
self.model_meta = {model["name"]: model for model in self.app.models}
|
|
174
|
+
self.selected_models = {
|
|
175
|
+
name for name, model in self.model_meta.items() if _model_is_loaded(model)
|
|
176
|
+
}
|
|
177
|
+
if not self.selected_models and len(self.model_order) == 1:
|
|
178
|
+
self.selected_models = set(self.model_order)
|
|
179
|
+
self.selected_benchmarks = {"sanity"} if "sanity" in REGISTRY else set()
|
|
180
|
+
|
|
181
|
+
self._rebuild_models()
|
|
182
|
+
self._rebuild_benchmarks()
|
|
183
|
+
self._refresh_summary()
|
|
184
|
+
self.query_one("#model-list", SelectionList).focus()
|
|
185
|
+
self.watch(self.app, "theme", self._theme_changed, init=False)
|
|
186
|
+
self._count_tasks()
|
|
187
|
+
|
|
188
|
+
def _theme_changed(self, _theme: str) -> None:
|
|
189
|
+
self._rebuild_benchmarks()
|
|
190
|
+
|
|
191
|
+
# Task counting ----------------------------------------------------
|
|
192
|
+
|
|
193
|
+
@work(thread=True, exclusive=True, group="counts")
|
|
194
|
+
def _count_tasks(self) -> None:
|
|
195
|
+
for key in list(REGISTRY):
|
|
196
|
+
try:
|
|
197
|
+
count = task_count(key)
|
|
198
|
+
except Exception:
|
|
199
|
+
count = -1
|
|
200
|
+
self.app.call_from_thread(self._count_ready, key, count)
|
|
201
|
+
|
|
202
|
+
def _count_ready(self, key: str, count: int) -> None:
|
|
203
|
+
self.counts[key] = count
|
|
204
|
+
self._rebuild_benchmarks()
|
|
205
|
+
self._refresh_summary()
|
|
206
|
+
|
|
207
|
+
# List construction ------------------------------------------------
|
|
208
|
+
|
|
209
|
+
def _model_prompt(self, name: str) -> Text:
|
|
210
|
+
meta = self.model_meta.get(name, {})
|
|
211
|
+
status = str(meta.get("status") or "")
|
|
212
|
+
if isinstance(meta.get("loaded"), bool):
|
|
213
|
+
detail = status or ("loaded" if meta["loaded"] else "unloaded")
|
|
214
|
+
else:
|
|
215
|
+
detail = (
|
|
216
|
+
fmt_size(meta.get("size")) or status or str(meta.get("owned_by") or "")
|
|
217
|
+
)
|
|
218
|
+
text = Text()
|
|
219
|
+
text.append(_clip(name, MODEL_NAME_WIDTH).ljust(MODEL_NAME_WIDTH))
|
|
220
|
+
status = self.template_status.get(name)
|
|
221
|
+
if status == "checking":
|
|
222
|
+
text.append("… checking".rjust(11), style="dim")
|
|
223
|
+
elif status == "pass":
|
|
224
|
+
text.append("✓ template".rjust(11), style="green")
|
|
225
|
+
elif status == "warn":
|
|
226
|
+
text.append("! template".rjust(11), style="yellow")
|
|
227
|
+
elif status == "fail":
|
|
228
|
+
text.append("✗ template".rjust(11), style="red")
|
|
229
|
+
elif status == "unavailable":
|
|
230
|
+
text.append("— n/a".rjust(11), style="dim")
|
|
231
|
+
elif meta.get("decision"):
|
|
232
|
+
text.append("decision".rjust(9), style="cyan")
|
|
233
|
+
else:
|
|
234
|
+
text.append(detail.rjust(9), style="dim")
|
|
235
|
+
return text
|
|
236
|
+
|
|
237
|
+
def _bench_prompt(self, key: str) -> Text:
|
|
238
|
+
count = self.counts.get(key)
|
|
239
|
+
custom_count = getattr(REGISTRY[key], "list_task_count", "")
|
|
240
|
+
if count is None:
|
|
241
|
+
count_text = "counting…"
|
|
242
|
+
elif count < 0:
|
|
243
|
+
count_text = "unavailable"
|
|
244
|
+
else:
|
|
245
|
+
count_text = f"{custom_count or fmt_count(count)} tasks"
|
|
246
|
+
|
|
247
|
+
text = Text()
|
|
248
|
+
text.append(_clip(key, BENCHMARK_NAME_WIDTH).ljust(BENCHMARK_NAME_WIDTH))
|
|
249
|
+
text.append(count_text.rjust(BENCHMARK_COUNT_WIDTH), style="dim")
|
|
250
|
+
limit = self.limits.get(key)
|
|
251
|
+
if limit:
|
|
252
|
+
accent = score_palette(self.app.current_theme.dark)["mid"]
|
|
253
|
+
text.append(f" {slice_label(limit)}", style=f"bold {accent}")
|
|
254
|
+
else:
|
|
255
|
+
if key in self.missing_data:
|
|
256
|
+
text.append(" ↓ download", style="yellow")
|
|
257
|
+
text.append(f" {DESCRIPTIONS.get(key, '')}", style="dim")
|
|
258
|
+
return text
|
|
259
|
+
|
|
260
|
+
def _rebuild_models(self) -> None:
|
|
261
|
+
widget = self.query_one("#model-list", SelectionList)
|
|
262
|
+
needle = self.query_one("#model-filter", Input).value.strip().lower()
|
|
263
|
+
visible = [name for name in self.model_order if needle in name.lower()]
|
|
264
|
+
highlighted = widget.highlighted
|
|
265
|
+
widget.clear_options()
|
|
266
|
+
widget.add_options(
|
|
267
|
+
[
|
|
268
|
+
Selection(self._model_prompt(name), name, name in self.selected_models)
|
|
269
|
+
for name in visible
|
|
270
|
+
]
|
|
271
|
+
)
|
|
272
|
+
if visible:
|
|
273
|
+
widget.highlighted = min(highlighted or 0, len(visible) - 1)
|
|
274
|
+
self.query_one("#model-title", SectionTitle).set_detail(
|
|
275
|
+
f"{len(self.selected_models)}/{len(self.model_order)} selected"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def _rebuild_benchmarks(self) -> None:
|
|
279
|
+
widget = self.query_one("#bench-list", SelectionList)
|
|
280
|
+
needle = self.query_one("#bench-filter", Input).value.strip().lower()
|
|
281
|
+
visible = [
|
|
282
|
+
key
|
|
283
|
+
for key in self.bench_order
|
|
284
|
+
if needle in key.lower() or needle in DESCRIPTIONS.get(key, "").lower()
|
|
285
|
+
]
|
|
286
|
+
highlighted = widget.highlighted
|
|
287
|
+
widget.clear_options()
|
|
288
|
+
widget.add_options(
|
|
289
|
+
[
|
|
290
|
+
Selection(self._bench_prompt(key), key, key in self.selected_benchmarks)
|
|
291
|
+
for key in visible
|
|
292
|
+
]
|
|
293
|
+
)
|
|
294
|
+
if visible:
|
|
295
|
+
widget.highlighted = min(highlighted or 0, len(visible) - 1)
|
|
296
|
+
self._refresh_bench_title(len(visible))
|
|
297
|
+
|
|
298
|
+
def _refresh_bench_title(self, shown: int | None = None) -> None:
|
|
299
|
+
detail = f"{len(self.selected_benchmarks)}/{len(self.bench_order)} selected"
|
|
300
|
+
if shown is None:
|
|
301
|
+
shown = self.query_one("#bench-list", SelectionList).option_count
|
|
302
|
+
if shown != len(self.bench_order):
|
|
303
|
+
detail = f"{detail} · {shown} shown"
|
|
304
|
+
self.query_one("#bench-title", SectionTitle).set_detail(detail)
|
|
305
|
+
|
|
306
|
+
# Events -----------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
309
|
+
if event.input.id == "model-filter":
|
|
310
|
+
self._rebuild_models()
|
|
311
|
+
elif event.input.id == "bench-filter":
|
|
312
|
+
self._rebuild_benchmarks()
|
|
313
|
+
elif event.input.id in {"global-limit", "repair-attempts"}:
|
|
314
|
+
self._refresh_summary()
|
|
315
|
+
|
|
316
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
317
|
+
if event.input.id in {"model-filter", "bench-filter"}:
|
|
318
|
+
target = (
|
|
319
|
+
"#model-list" if event.input.id == "model-filter" else "#bench-list"
|
|
320
|
+
)
|
|
321
|
+
self.query_one(target, SelectionList).focus()
|
|
322
|
+
elif event.input.id in {
|
|
323
|
+
"global-limit",
|
|
324
|
+
"perturbation-seed",
|
|
325
|
+
"repair-attempts",
|
|
326
|
+
}:
|
|
327
|
+
self.action_start()
|
|
328
|
+
|
|
329
|
+
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
|
330
|
+
if event.checkbox.id != "choice-order":
|
|
331
|
+
return
|
|
332
|
+
self.query_one("#perturbation-seed", Input).disabled = not event.value
|
|
333
|
+
self._refresh_summary()
|
|
334
|
+
|
|
335
|
+
def on_select_changed(self, event: Select.Changed) -> None:
|
|
336
|
+
if event.select.id == "harness":
|
|
337
|
+
self._refresh_summary()
|
|
338
|
+
|
|
339
|
+
def on_selection_list_selected_changed(
|
|
340
|
+
self, event: SelectionList.SelectedChanged
|
|
341
|
+
) -> None:
|
|
342
|
+
widget = event.selection_list
|
|
343
|
+
visible = {
|
|
344
|
+
widget.get_option_at_index(index).value
|
|
345
|
+
for index in range(widget.option_count)
|
|
346
|
+
}
|
|
347
|
+
chosen = set(widget.selected)
|
|
348
|
+
if widget.id == "model-list":
|
|
349
|
+
self.selected_models = (self.selected_models - visible) | chosen
|
|
350
|
+
self.query_one("#model-title", SectionTitle).set_detail(
|
|
351
|
+
f"{len(self.selected_models)}/{len(self.model_order)} selected"
|
|
352
|
+
)
|
|
353
|
+
else:
|
|
354
|
+
self.selected_benchmarks = (self.selected_benchmarks - visible) | chosen
|
|
355
|
+
self._refresh_bench_title()
|
|
356
|
+
self._refresh_summary()
|
|
357
|
+
|
|
358
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
359
|
+
if event.button.id == "start":
|
|
360
|
+
self.action_start()
|
|
361
|
+
elif event.button.id == "check-templates":
|
|
362
|
+
self.action_check_templates()
|
|
363
|
+
elif event.button.id == "datasets":
|
|
364
|
+
self.action_datasets()
|
|
365
|
+
|
|
366
|
+
# Actions ----------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
def _focused_list(self) -> SelectionList | None:
|
|
369
|
+
node = self.focused
|
|
370
|
+
while node is not None:
|
|
371
|
+
if isinstance(node, SelectionList):
|
|
372
|
+
return node
|
|
373
|
+
node = node.parent
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
def action_select_all(self) -> None:
|
|
377
|
+
widget = self._focused_list()
|
|
378
|
+
if widget is not None:
|
|
379
|
+
widget.select_all()
|
|
380
|
+
|
|
381
|
+
def action_select_none(self) -> None:
|
|
382
|
+
widget = self._focused_list()
|
|
383
|
+
if widget is not None:
|
|
384
|
+
widget.deselect_all()
|
|
385
|
+
|
|
386
|
+
def action_invert(self) -> None:
|
|
387
|
+
widget = self._focused_list()
|
|
388
|
+
if widget is not None:
|
|
389
|
+
widget.toggle_all()
|
|
390
|
+
|
|
391
|
+
def action_focus_filter(self) -> None:
|
|
392
|
+
widget = self._focused_list()
|
|
393
|
+
target = (
|
|
394
|
+
"#bench-filter"
|
|
395
|
+
if widget is not None and widget.id == "bench-list"
|
|
396
|
+
else "#model-filter"
|
|
397
|
+
)
|
|
398
|
+
self.query_one(target, Input).focus()
|
|
399
|
+
|
|
400
|
+
def action_set_limit(self) -> None:
|
|
401
|
+
widget = self.query_one("#bench-list", SelectionList)
|
|
402
|
+
index = widget.highlighted
|
|
403
|
+
if index is None:
|
|
404
|
+
return
|
|
405
|
+
key = widget.get_option_at_index(index).value
|
|
406
|
+
total = slice_task_count(key) if self.counts.get(key, 0) >= 0 else -1
|
|
407
|
+
|
|
408
|
+
def apply(spec: str | None) -> None:
|
|
409
|
+
if spec:
|
|
410
|
+
self.limits[key] = spec
|
|
411
|
+
else:
|
|
412
|
+
self.limits.pop(key, None)
|
|
413
|
+
self._rebuild_benchmarks()
|
|
414
|
+
self._refresh_summary()
|
|
415
|
+
|
|
416
|
+
self.app.push_screen(LimitScreen(key, self.limits.get(key, ""), total), apply)
|
|
417
|
+
|
|
418
|
+
def action_toggle_perturbation(self) -> None:
|
|
419
|
+
checkbox = self.query_one("#choice-order", Checkbox)
|
|
420
|
+
checkbox.value = not checkbox.value
|
|
421
|
+
|
|
422
|
+
def action_check_templates(self) -> None:
|
|
423
|
+
targets = [
|
|
424
|
+
name
|
|
425
|
+
for name in self.model_order
|
|
426
|
+
if not self.selected_models or name in self.selected_models
|
|
427
|
+
]
|
|
428
|
+
if not targets:
|
|
429
|
+
return
|
|
430
|
+
if self.app.demo:
|
|
431
|
+
self.notify("Template checks require a llama.cpp or llama-swap server.")
|
|
432
|
+
return
|
|
433
|
+
for name in targets:
|
|
434
|
+
self.template_status[name] = "checking"
|
|
435
|
+
self.checking_templates = True
|
|
436
|
+
self._rebuild_models()
|
|
437
|
+
self.query_one("#check-templates", Button).disabled = True
|
|
438
|
+
self.query_one("#start", Button).disabled = True
|
|
439
|
+
self._run_template_checks(targets)
|
|
440
|
+
|
|
441
|
+
@work(thread=True, exclusive=True, group="template-checks")
|
|
442
|
+
def _run_template_checks(self, targets: list[str]) -> None:
|
|
443
|
+
reports: list[TemplateReport] = []
|
|
444
|
+
for name in targets:
|
|
445
|
+
report = check_template(self.app.client, name)
|
|
446
|
+
reports.append(report)
|
|
447
|
+
self.app.call_from_thread(self._template_check_ready, report)
|
|
448
|
+
self.app.call_from_thread(self._template_checks_finished, reports)
|
|
449
|
+
|
|
450
|
+
def _template_check_ready(self, report: TemplateReport) -> None:
|
|
451
|
+
self.template_status[report.model] = report.status
|
|
452
|
+
self._rebuild_models()
|
|
453
|
+
|
|
454
|
+
def _template_checks_finished(self, reports: list[TemplateReport]) -> None:
|
|
455
|
+
self.checking_templates = False
|
|
456
|
+
self.query_one("#check-templates", Button).disabled = False
|
|
457
|
+
self.query_one("#start", Button).disabled = False
|
|
458
|
+
self.app.push_screen(TemplateCheckScreen(reports))
|
|
459
|
+
|
|
460
|
+
def action_datasets(self) -> None:
|
|
461
|
+
def refresh(installed: bool | None) -> None:
|
|
462
|
+
if installed:
|
|
463
|
+
self.missing_data = _missing_data()
|
|
464
|
+
self._rebuild_benchmarks()
|
|
465
|
+
|
|
466
|
+
self.app.push_screen(DatasetsScreen(), refresh)
|
|
467
|
+
|
|
468
|
+
def action_back(self) -> None:
|
|
469
|
+
if self.checking_templates:
|
|
470
|
+
self.notify("Wait for the template check to finish.", timeout=3)
|
|
471
|
+
return
|
|
472
|
+
self.app.pop_screen()
|
|
473
|
+
|
|
474
|
+
def action_start(self) -> None:
|
|
475
|
+
if self.checking_templates:
|
|
476
|
+
self.notify("Wait for the template check to finish.", timeout=3)
|
|
477
|
+
return
|
|
478
|
+
jobs = self._build_jobs()
|
|
479
|
+
if jobs is None:
|
|
480
|
+
return
|
|
481
|
+
if self.app.demo and isinstance(self.app.client, DemoClient):
|
|
482
|
+
perturbations = sorted(
|
|
483
|
+
{
|
|
484
|
+
(job.perturbation, job.perturbation_seed)
|
|
485
|
+
for job in jobs
|
|
486
|
+
if job.perturbation
|
|
487
|
+
}
|
|
488
|
+
)
|
|
489
|
+
self.app.client.prime(
|
|
490
|
+
sorted({job.benchmark for job in jobs}),
|
|
491
|
+
perturbations=perturbations,
|
|
492
|
+
)
|
|
493
|
+
fetching = sorted({job.benchmark for job in jobs} & self.missing_data)
|
|
494
|
+
if fetching:
|
|
495
|
+
self.notify(
|
|
496
|
+
f"Downloading {', '.join(fetching)} before the first task.",
|
|
497
|
+
timeout=6,
|
|
498
|
+
)
|
|
499
|
+
self.app.start_run(jobs, force_unload=self._force_unload_enabled())
|
|
500
|
+
|
|
501
|
+
# Planning ---------------------------------------------------------
|
|
502
|
+
|
|
503
|
+
def _global_limit(self) -> str:
|
|
504
|
+
return self.query_one("#global-limit", Input).value.strip()
|
|
505
|
+
|
|
506
|
+
def _limit_for(self, key: str) -> str | None:
|
|
507
|
+
return self.limits.get(key) or self._global_limit() or None
|
|
508
|
+
|
|
509
|
+
def _choice_order_enabled(self) -> bool:
|
|
510
|
+
return self.query_one("#choice-order", Checkbox).value
|
|
511
|
+
|
|
512
|
+
def _force_unload_enabled(self) -> bool:
|
|
513
|
+
return self.query_one("#force-unload", Checkbox).value
|
|
514
|
+
|
|
515
|
+
def _harnesses(self) -> tuple[str, ...]:
|
|
516
|
+
value = self.query_one("#harness", Select).value
|
|
517
|
+
return ("direct", "pi") if value == "both" else (str(value),)
|
|
518
|
+
|
|
519
|
+
def _repair_attempts(self) -> int:
|
|
520
|
+
value = self.query_one("#repair-attempts", Input).value.strip()
|
|
521
|
+
try:
|
|
522
|
+
return int(value)
|
|
523
|
+
except ValueError:
|
|
524
|
+
return -1
|
|
525
|
+
|
|
526
|
+
def _perturbation_seed(self) -> int | None:
|
|
527
|
+
value = self.query_one("#perturbation-seed", Input).value.strip()
|
|
528
|
+
try:
|
|
529
|
+
return int(value)
|
|
530
|
+
except ValueError:
|
|
531
|
+
return None
|
|
532
|
+
|
|
533
|
+
def _build_jobs(self) -> list[JobSpec] | None:
|
|
534
|
+
models = [name for name in self.model_order if name in self.selected_models]
|
|
535
|
+
benches = [key for key in self.bench_order if key in self.selected_benchmarks]
|
|
536
|
+
|
|
537
|
+
if not models:
|
|
538
|
+
self.notify("Select at least one model", severity="error", timeout=4)
|
|
539
|
+
self.query_one("#model-list", SelectionList).focus()
|
|
540
|
+
return None
|
|
541
|
+
if not benches:
|
|
542
|
+
self.notify("Select at least one benchmark", severity="error", timeout=4)
|
|
543
|
+
self.query_one("#bench-list", SelectionList).focus()
|
|
544
|
+
return None
|
|
545
|
+
if self.app.demo and "pi" in self._harnesses():
|
|
546
|
+
self.notify(
|
|
547
|
+
"Pi harness is unavailable in demo mode.",
|
|
548
|
+
severity="error",
|
|
549
|
+
timeout=5,
|
|
550
|
+
)
|
|
551
|
+
self.query_one("#harness", Select).focus()
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
repair_attempts = self._repair_attempts()
|
|
555
|
+
if not 0 <= repair_attempts <= MAX_REPAIR_ATTEMPTS:
|
|
556
|
+
self.notify(
|
|
557
|
+
f"Repair attempts must be between 0 and {MAX_REPAIR_ATTEMPTS}.",
|
|
558
|
+
severity="error",
|
|
559
|
+
timeout=4,
|
|
560
|
+
)
|
|
561
|
+
self.query_one("#repair-attempts", Input).focus()
|
|
562
|
+
return None
|
|
563
|
+
|
|
564
|
+
choice_order = self._choice_order_enabled()
|
|
565
|
+
seed = self._perturbation_seed()
|
|
566
|
+
if choice_order and seed is None:
|
|
567
|
+
self.notify(
|
|
568
|
+
"Choice-order seed must be an integer.",
|
|
569
|
+
severity="error",
|
|
570
|
+
timeout=4,
|
|
571
|
+
)
|
|
572
|
+
self.query_one("#perturbation-seed", Input).focus()
|
|
573
|
+
return None
|
|
574
|
+
if choice_order:
|
|
575
|
+
unsupported = unsupported_benchmarks(CHOICE_ORDER, benches)
|
|
576
|
+
if unsupported:
|
|
577
|
+
self.notify(
|
|
578
|
+
"Choice-order is unsupported for: " + ", ".join(unsupported),
|
|
579
|
+
severity="error",
|
|
580
|
+
timeout=6,
|
|
581
|
+
)
|
|
582
|
+
self.query_one("#bench-list", SelectionList).focus()
|
|
583
|
+
return None
|
|
584
|
+
|
|
585
|
+
for key in benches:
|
|
586
|
+
total = slice_task_count(key) if self.counts.get(key, 0) >= 0 else -1
|
|
587
|
+
if total < 0:
|
|
588
|
+
self.notify(
|
|
589
|
+
f"{key}: dataset could not be loaded.",
|
|
590
|
+
severity="error",
|
|
591
|
+
timeout=6,
|
|
592
|
+
)
|
|
593
|
+
return None
|
|
594
|
+
spec = self._limit_for(key)
|
|
595
|
+
try:
|
|
596
|
+
# A count of 0 means the tally is still running, which
|
|
597
|
+
# parse_slice reads as "syntax only".
|
|
598
|
+
parse_slice(spec, max(total, 0))
|
|
599
|
+
except SliceError:
|
|
600
|
+
self.notify(
|
|
601
|
+
f"{key}: {spec!r} is not a valid slice. Use N, -N or A-B.",
|
|
602
|
+
severity="error",
|
|
603
|
+
timeout=6,
|
|
604
|
+
)
|
|
605
|
+
return None
|
|
606
|
+
|
|
607
|
+
jobs = job_matrix(
|
|
608
|
+
models,
|
|
609
|
+
[(key, self._limit_for(key), None) for key in benches],
|
|
610
|
+
self._harnesses(),
|
|
611
|
+
repair_attempts=repair_attempts,
|
|
612
|
+
**(
|
|
613
|
+
{"perturbation": CHOICE_ORDER, "perturbation_seed": seed}
|
|
614
|
+
if choice_order and seed is not None
|
|
615
|
+
else {}
|
|
616
|
+
),
|
|
617
|
+
)
|
|
618
|
+
try:
|
|
619
|
+
jobs = route_decision_jobs(jobs, self.app.client)
|
|
620
|
+
except ValueError as exc:
|
|
621
|
+
self.notify(str(exc), severity="error", timeout=8)
|
|
622
|
+
self.query_one("#bench-list", SelectionList).focus()
|
|
623
|
+
return None
|
|
624
|
+
expanded = expand_jobs(jobs, self.app.client)
|
|
625
|
+
if not expanded:
|
|
626
|
+
self.notify(
|
|
627
|
+
"No supported RULER context bucket fits the selected model(s).",
|
|
628
|
+
severity="error",
|
|
629
|
+
timeout=6,
|
|
630
|
+
)
|
|
631
|
+
return None
|
|
632
|
+
return expanded
|
|
633
|
+
|
|
634
|
+
def _planned_tasks(self, key: str) -> int:
|
|
635
|
+
loaded_total = self.counts.get(key, 0)
|
|
636
|
+
if loaded_total <= 0:
|
|
637
|
+
return 0
|
|
638
|
+
total = slice_task_count(key)
|
|
639
|
+
try:
|
|
640
|
+
start, end = parse_slice(self._limit_for(key), total)
|
|
641
|
+
except SliceError:
|
|
642
|
+
return 0
|
|
643
|
+
return max(0, end - start)
|
|
644
|
+
|
|
645
|
+
def _variant_count(self, key: str, model: str) -> int:
|
|
646
|
+
buckets = getattr(REGISTRY[key], "context_buckets", ())
|
|
647
|
+
if not buckets:
|
|
648
|
+
return 1
|
|
649
|
+
context = self.model_meta.get(model, {}).get("context_length")
|
|
650
|
+
if isinstance(context, int) and not isinstance(context, bool) and context > 0:
|
|
651
|
+
return sum(bucket <= context for bucket in buckets)
|
|
652
|
+
return len(buckets)
|
|
653
|
+
|
|
654
|
+
def _refresh_summary(self) -> None:
|
|
655
|
+
models = len(self.selected_models)
|
|
656
|
+
benches = [key for key in self.bench_order if key in self.selected_benchmarks]
|
|
657
|
+
choice_order = self._choice_order_enabled()
|
|
658
|
+
unsupported = (
|
|
659
|
+
unsupported_benchmarks(CHOICE_ORDER, benches) if choice_order else []
|
|
660
|
+
)
|
|
661
|
+
multiplier = (2 if choice_order else 1) * len(self._harnesses())
|
|
662
|
+
runs = (
|
|
663
|
+
sum(
|
|
664
|
+
self._variant_count(key, model)
|
|
665
|
+
for model in self.selected_models
|
|
666
|
+
for key in benches
|
|
667
|
+
)
|
|
668
|
+
* multiplier
|
|
669
|
+
)
|
|
670
|
+
tasks = (
|
|
671
|
+
sum(
|
|
672
|
+
self._planned_tasks(key) * self._variant_count(key, model)
|
|
673
|
+
for model in self.selected_models
|
|
674
|
+
for key in benches
|
|
675
|
+
)
|
|
676
|
+
* multiplier
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
if runs == 0:
|
|
680
|
+
summary = Content.from_markup(
|
|
681
|
+
"[dim]Nothing selected yet — pick models and benchmarks.[/dim]"
|
|
682
|
+
)
|
|
683
|
+
elif unsupported:
|
|
684
|
+
summary = Content.from_markup(
|
|
685
|
+
"[red]Choice-order unsupported for: $unsupported[/red]"
|
|
686
|
+
"[dim] — change the benchmark selection or turn it off.[/dim]",
|
|
687
|
+
unsupported=", ".join(unsupported),
|
|
688
|
+
)
|
|
689
|
+
else:
|
|
690
|
+
summary = Content.from_markup(
|
|
691
|
+
"[b]$models[/b][dim] model$ms × [/dim][b]$benches[/b][dim] benchmark$bs"
|
|
692
|
+
" = [/dim][b]$runs[/b][dim] run$rs · [/dim][b]$tasks[/b][dim] tasks total"
|
|
693
|
+
"$paired[/dim]",
|
|
694
|
+
models=models,
|
|
695
|
+
ms="" if models == 1 else "s",
|
|
696
|
+
benches=len(benches),
|
|
697
|
+
bs="" if len(benches) == 1 else "s",
|
|
698
|
+
runs=runs,
|
|
699
|
+
rs="" if runs == 1 else "s",
|
|
700
|
+
tasks=fmt_count(tasks),
|
|
701
|
+
paired=(
|
|
702
|
+
(" · clean + choice-order" if choice_order else "")
|
|
703
|
+
+ (
|
|
704
|
+
f" · {self._repair_attempts()} verifier repair turn(s)"
|
|
705
|
+
if self._repair_attempts() > 0
|
|
706
|
+
else ""
|
|
707
|
+
)
|
|
708
|
+
+ (
|
|
709
|
+
" · force unload between models"
|
|
710
|
+
if self._force_unload_enabled()
|
|
711
|
+
else ""
|
|
712
|
+
)
|
|
713
|
+
),
|
|
714
|
+
)
|
|
715
|
+
self.query_one("#plan-summary", Static).update(summary)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _clip(text: str, width: int) -> str:
|
|
719
|
+
return text if len(text) <= width else text[: width - 1] + "…"
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _model_is_loaded(model: dict) -> bool:
|
|
723
|
+
"""Return whether discovery positively identified a resident model."""
|
|
724
|
+
loaded = model.get("loaded")
|
|
725
|
+
if isinstance(loaded, bool):
|
|
726
|
+
return loaded
|
|
727
|
+
status = str(model.get("status") or "").strip().lower()
|
|
728
|
+
return status in {"loaded", "running"}
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _missing_data() -> set[str]:
|
|
732
|
+
"""Benchmarks whose data a run would have to download first."""
|
|
733
|
+
return {entry.benchmark for entry in catalog.statuses() if entry.installable}
|