tuieval 0.2.0.dev55__tar.gz → 0.2.0.dev62__tar.gz
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-0.2.0.dev55 → tuieval-0.2.0.dev62}/CHANGELOG.md +3 -1
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/PKG-INFO +3 -3
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/README.md +2 -2
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/models.md +6 -4
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/writing-packs.md +2 -2
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/cli.py +1 -1
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/compare.py +20 -5
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/engine.py +17 -7
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/pta.py +71 -36
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/run_evals.py +4 -3
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/tui.py +66 -117
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/verdict.py +2 -1
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/tests/test_tuieval.py +67 -15
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/.gitignore +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/LICENSE +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/doctor.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/machines.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/tune.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev55 → tuieval-0.2.0.dev62}/tests/mock_server.py +0 -0
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Results has four tabs: PTA index, Production readiness, Per question and Failures. The PTA table adds tok/s, TTFT and memory; Per question can show only where models disagree; the settings warning is one line. Scorecard, pairwise, history and test quality stay in `tuieval compare`, `history` and `items`.
|
|
6
|
+
- The run's ETA adds up each pack's answers left × that model's time per answer there (its history, then its own times once it has 5 answers), instead of extrapolating from the first few answers.
|
|
5
7
|
- Faster start on Macs: the GPU core count comes from `ioreg` instead of `system_profiler`, which took seconds on some Macs.
|
|
6
8
|
- A model a server won't serve (HTTP 401-404, e.g. OpenRouter's "No endpoints found") is never judged on it: those answers don't count, and the model stops at once instead of failing every question.
|
|
7
|
-
- PTA index: parsimony (tokens
|
|
9
|
+
- PTA index: parsimony (tokens), time and accuracy per model as bars, with tokens and time shown as multiples of the best (2.4×): the first Results tab (where `r` lands), `tuieval pta` and the report. Server errors aren't answers, and models with few answers are left out unless picked.
|
|
8
10
|
- Run several models at a time: `parallel_models` per machine, **Models at a time** in the TUI, or `tuieval run --parallel N` (default 1). Models wait for memory to fit, and answers note what ran alongside them.
|
|
9
11
|
- More evals during a run: `n` opens Setup, and Start adds them to the run going (same tier) or queues them after it.
|
|
10
12
|
- A new workspace's `models.toml` is 23 lines: the llama, local and OpenRouter servers and the defaults are built in (docs/models.md lists them in full; a server defined in `models.toml` replaces the built-in one with that name). Existing workspaces keep their settings; built-in servers they don't define become available, and adding a model still prefers the workspace's own servers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tuieval
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev62
|
|
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
|
|
@@ -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
|
|
|
@@ -118,7 +118,7 @@ See **[docs/models.md](docs/models.md)** for servers, OpenRouter endpoint pinnin
|
|
|
118
118
|
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.
|
|
119
119
|
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.
|
|
120
120
|
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.
|
|
121
|
-
4. **Press `r` for results:**
|
|
121
|
+
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.
|
|
122
122
|
|
|
123
123
|
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.
|
|
124
124
|
|
|
@@ -150,7 +150,7 @@ tuieval help
|
|
|
150
150
|
|
|
151
151
|
- **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.
|
|
152
152
|
- **INCONCLUSIVE is not "nearly passed".** It says what's missing (usually a Certify run, or more trials).
|
|
153
|
-
- **Trust
|
|
153
|
+
- **Trust `tuieval compare --pairwise` over raw percentages.** Differences of one or two tests are usually noise.
|
|
154
154
|
- **Read `trunc` before accuracy.** A model that runs out of tokens isn't wrong; it's thinking too long for the budget.
|
|
155
155
|
- **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.
|
|
156
156
|
|
|
@@ -86,11 +86,13 @@ By default a run serves one model at a time. On a machine with room for more, se
|
|
|
86
86
|
|
|
87
87
|
## PTA index
|
|
88
88
|
|
|
89
|
-
The **PTA index** puts parsimony (tokens),
|
|
89
|
+
The **PTA index** puts parsimony (tokens), time and accuracy side by side, as a bar for each per model (longer is better), with a table underneath. It's the first tab in Results, where `r` opens, and it's also `tuieval pta` and a section in `tuieval report`. There's no combined number; verdicts and critical failures stay in Production readiness.
|
|
90
90
|
|
|
91
|
-
- **P parsimony
|
|
92
|
-
- **T
|
|
93
|
-
- **A accuracy
|
|
91
|
+
- **P parsimony (tokens)**: the tokens used, reasoning and answer, on every question compared (each question at its median over repeats). Beside the bar: how many times the leanest model's, e.g. `2.4×` (`1.0×` is the leanest, ★ in the table's **tokens/answer**). Unlike time it doesn't depend on your machine: it's how much a model says to get to an answer, which is what you pay for on an API and what fills its context.
|
|
92
|
+
- **T time**: the total time to answer those questions, as how many times the fastest model's, e.g. `28×` (★ in **time/answer**).
|
|
93
|
+
- **A accuracy**: the share of answers to those questions that passed.
|
|
94
|
+
|
|
95
|
+
The P and T bars use a log scale: full for the best, 15 points (of 100) shorter for every doubling, empty at about 100 times the best. So a model at 28× still shows a bar, and adding a better model shortens the others'.
|
|
94
96
|
|
|
95
97
|
Models are compared on the questions all of them answered, so one that skipped some never looks faster. Server and connection errors aren't answers: a model with only those is listed as having none. A model with fewer than half as many answers as the most-answered one is left out (and listed), or it would shrink the shared questions for everyone. Pick the models and pack to compare in the TUI, or use `--only` and `--packs`: models you pick are always compared.
|
|
96
98
|
|
|
@@ -127,7 +127,7 @@ def ticket_route(answer, test, meta):
|
|
|
127
127
|
- `critical=True` makes every test of this grader a critical trial (any of its failures *can* be critical). Otherwise only tests with `critical: true` or `expected: NOT_AVAILABLE` are.
|
|
128
128
|
- `template` is the skeleton `tuieval capture` writes for a new test of this grader.
|
|
129
129
|
|
|
130
|
-
A grader file can also add **Scorecard columns** (
|
|
130
|
+
A grader file can also add **Scorecard columns** (`tuieval compare`), for numbers your use case cares about that pass/fail doesn't show:
|
|
131
131
|
|
|
132
132
|
```python
|
|
133
133
|
import json
|
|
@@ -175,7 +175,7 @@ Graders mark failures that should disqualify a model whatever its accuracy:
|
|
|
175
175
|
|
|
176
176
|
## Difficulty labels
|
|
177
177
|
|
|
178
|
-
Every test has a `difficulty` of easy, medium or hard. It's for you, not the model: requests are built only from `input`, `image`, the system prompt and tools, so the label never reaches the model. The TUI shows it in the pack list (e.g. `12E 20M 8H`), next to the current question, in Recent results and Failures, and in Results →
|
|
178
|
+
Every test has a `difficulty` of easy, medium or hard. It's for you, not the model: requests are built only from `input`, `image`, the system prompt and tools, so the label never reaches the model. The TUI shows it in the pack list (e.g. `12E 20M 8H`), next to the current question, in Recent results and Failures, and as a filter in Results → Per question.
|
|
179
179
|
|
|
180
180
|
A useful convention: **easy** = one rule or a direct read; **medium** = arithmetic, one trap, or two conditions; **hard** = rules in conflict, multi-step reasoning, or misleading data. Once several models have run, `tuieval items` flags labels the results contradict ("easier than labelled": a hard test every model passes; "harder than labelled": an easy test most models fail). `tuieval selftest` warns about tests without a label.
|
|
181
181
|
|
|
@@ -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.dev62'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev62')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -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
|
|
@@ -121,6 +121,12 @@ def p90(values):
|
|
|
121
121
|
return statistics.quantiles(values, n=10)[-1] if len(values) > 1 else (values[0] if values else None)
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
def pct(fraction):
|
|
125
|
+
"""A share as a percentage: "100%" and "0%" only when exact, else one decimal ("99.6%"), so a model
|
|
126
|
+
with a wrong answer never shows as perfect."""
|
|
127
|
+
return "100%" if fraction >= 1 else "0%" if fraction <= 0 else f"{100 * fraction:.1f}%"
|
|
128
|
+
|
|
129
|
+
|
|
124
130
|
def median(values):
|
|
125
131
|
values = [v for v in values if v is not None]
|
|
126
132
|
return statistics.median(values) if values else None
|
|
@@ -154,10 +160,19 @@ def settings_notes(infos):
|
|
|
154
160
|
per_model.setdefault(i["label"], i["settings"])
|
|
155
161
|
keys = sorted({k for s in per_model.values() for k in s})
|
|
156
162
|
differing = [k for k in keys if len({json.dumps(s.get(k)) for s in per_model.values()}) > 1]
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
parts = []
|
|
164
|
+
for k in differing: # only the models that differ from the most common value, named when few
|
|
165
|
+
groups = collections.defaultdict(list)
|
|
166
|
+
for m in sorted(per_model):
|
|
167
|
+
groups[json.dumps(per_model[m].get(k))].append(m)
|
|
168
|
+
common = max(groups, key=lambda v: len(groups[v]))
|
|
169
|
+
for value, ms in sorted(groups.items()):
|
|
170
|
+
if value != common:
|
|
171
|
+
who = ", ".join(ms) if len(ms) <= 3 else f"{len(ms)} models"
|
|
172
|
+
shown = "unset" if value == "null" else json.loads(value)
|
|
173
|
+
parts.append(f"{k} {shown} on {who}")
|
|
174
|
+
if parts:
|
|
175
|
+
notes.append("Settings differ: " + "; ".join(parts) + ".")
|
|
161
176
|
fps = collections.defaultdict(set)
|
|
162
177
|
for i in infos:
|
|
163
178
|
if i.get("pack"):
|
|
@@ -412,7 +427,7 @@ def by_difficulty(rows):
|
|
|
412
427
|
cells = []
|
|
413
428
|
for lvl in LEVELS:
|
|
414
429
|
x = [r["ok"] for r in sub if r.get("difficulty") == lvl]
|
|
415
|
-
cells.append(f"{sum(x)}/{len(x)} ({
|
|
430
|
+
cells.append(f"{sum(x)}/{len(x)} ({pct(sum(x) / len(x))})" if x else "-")
|
|
416
431
|
table.append([m, s] + cells)
|
|
417
432
|
return header, table
|
|
418
433
|
|
|
@@ -987,16 +987,16 @@ class Engine:
|
|
|
987
987
|
del per_pack[name]
|
|
988
988
|
return {l: p for l, p in out.items() if p}
|
|
989
989
|
|
|
990
|
-
def
|
|
991
|
-
"""(
|
|
990
|
+
def seconds_per_answer(self, jobs, waiting_only=True):
|
|
991
|
+
"""({job key: expected seconds per answer}, borrowed, guessed, thin) for the waiting jobs.
|
|
992
992
|
A pack the model has answered uses its own median time there. Otherwise the median of
|
|
993
993
|
other models' times on that pack, scaled by how much slower or faster this model is than
|
|
994
|
-
them on packs both have answered.
|
|
994
|
+
them on packs both have answered. The sets say which estimates are rough (see estimate_seconds)."""
|
|
995
995
|
times = self.answer_times()
|
|
996
996
|
med = {l: {p: statistics.median(ts) for p, ts in pp.items()} for l, pp in times.items()}
|
|
997
|
-
|
|
997
|
+
spr_of, borrowed, guessed, thin = {}, set(), set(), set()
|
|
998
998
|
for j in jobs:
|
|
999
|
-
if j.status != "waiting":
|
|
999
|
+
if j.status in ("done", "skipped", "failed") or (waiting_only and j.status != "waiting"):
|
|
1000
1000
|
continue
|
|
1001
1001
|
own = med.get(j.label, {})
|
|
1002
1002
|
ref = {p: statistics.median(v[p] for l, v in med.items() if p in v and l != j.label)
|
|
@@ -1013,8 +1013,18 @@ class Engine:
|
|
|
1013
1013
|
else:
|
|
1014
1014
|
spr = statistics.median(own.values()) if own else DEFAULT_SECONDS_PER_REQUEST
|
|
1015
1015
|
guessed.add((j.key, bool(own)))
|
|
1016
|
-
|
|
1017
|
-
|
|
1016
|
+
spr_of[j.key] = spr
|
|
1017
|
+
return spr_of, borrowed, guessed, thin
|
|
1018
|
+
|
|
1019
|
+
def estimate_seconds(self, jobs):
|
|
1020
|
+
"""(seconds, notes): expected run time for the waiting jobs, estimated per pack
|
|
1021
|
+
(seconds_per_answer). notes lists what made the estimate rough ([] = solid)."""
|
|
1022
|
+
spr_of, borrowed, guessed, thin = self.seconds_per_answer(jobs)
|
|
1023
|
+
total = 0.0
|
|
1024
|
+
for j in jobs:
|
|
1025
|
+
if j.key in spr_of:
|
|
1026
|
+
done = len(self._done_keys(j)) if j.merge else 0
|
|
1027
|
+
total += max(0, j.total - done) * spr_of[j.key]
|
|
1018
1028
|
notes = []
|
|
1019
1029
|
if borrowed:
|
|
1020
1030
|
factors = sorted({f for _, f, _ in borrowed if f is not None})
|
|
@@ -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):
|
|
@@ -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()
|