tuieval 0.2.0.dev62__tar.gz → 0.2.0.dev66__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.dev62 → tuieval-0.2.0.dev66}/CHANGELOG.md +2 -1
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/PKG-INFO +3 -3
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/README.md +2 -2
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/models.md +17 -5
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/_version.py +2 -2
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/compare.py +2 -1
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/doctor.py +17 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/engine.py +39 -10
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/machines.py +163 -5
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/pta.py +41 -21
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/tui.py +4 -3
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/tune.py +9 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/tests/test_tuieval.py +157 -5
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/.gitignore +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/LICENSE +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/RELEASING.md +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/README.md +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/favicon.ico +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/readme-header.png +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/social-preview.png +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/tuieval-icon-1024.png +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/tuieval-icon-128.png +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/tuieval-icon-16.png +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/tuieval-icon-16.svg +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/tuieval-icon-256.png +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/tuieval-icon-32.png +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/tuieval-icon-48.png +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/tuieval-icon-512.png +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/tuieval-icon-64.png +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/tuieval-icon-animated.svg +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/brand/tuieval-icon.svg +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/tui-answer-reasoning.png +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/tui-run-reasoning.png +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/images/tui-setup.png +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/docs/writing-packs.md +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/pyproject.toml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/__init__.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/__main__.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/builtin.toml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/cli.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/client.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/export.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/graders/__init__.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/graders/answer.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/graders/code.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/graders/rag.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/graders/reply.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/graders/tool_call.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/onboard.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/packs.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/profiles.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/remove.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/run_evals.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/scaffold.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/selftest.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/models.toml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/answer/pack.toml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/answer/system.txt +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/answer/tests.yaml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/code/pack.toml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/code/system.txt +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/code/tests.yaml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/rag/pack.toml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/rag/system.txt +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/rag/tests.yaml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/reply/pack.toml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/reply/system.txt +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/reply/tests.yaml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/tool_call/pack.toml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/tool_call/system.txt +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/tool_call/tests.yaml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/tool_call/tools.yaml +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/verdict.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/watch_proxy.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/workspace.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/yamlout.py +0 -0
- {tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/tests/mock_server.py +0 -0
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Nvidia and AMD GPUs on Linux: VRAM from `nvidia-smi`, `amd-smi` or `rocm-smi` sizes models (`gpu_memory_gb` overrides it), runs record peak VRAM, and `tuieval doctor` shows the GPU.
|
|
5
6
|
- 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
7
|
- 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.
|
|
7
8
|
- Faster start on Macs: the GPU core count comes from `ioreg` instead of `system_profiler`, which took seconds on some Macs.
|
|
8
9
|
- 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.
|
|
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.
|
|
10
|
+
- PTA index: parsimony (tokens), time and accuracy per model as bars (tokens and time as long as they took, shorter is better; accuracy longer is better), with tokens and time also 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.
|
|
10
11
|
- 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.
|
|
11
12
|
- More evals during a run: `n` opens Setup, and Start adds them to the run going (same tier) or queues them after it.
|
|
12
13
|
- 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.dev66
|
|
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
|
|
|
@@ -25,7 +25,7 @@ Whatever runs your models, the first steps are short:
|
|
|
25
25
|
| You have | Do this |
|
|
26
26
|
|---|---|
|
|
27
27
|
| **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. |
|
|
28
|
-
| **GGUF files** | Install llama.cpp (`brew install llama.cpp
|
|
28
|
+
| **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. |
|
|
29
29
|
| **An OpenRouter key** | `export OPENROUTER_API_KEY=…`, then `tuieval run --tier smoke --only openrouter:<model id>`. |
|
|
30
30
|
|
|
31
31
|
Stuck? `tuieval doctor` checks servers, models, packs and keys, and says what to fix.
|
|
@@ -50,7 +50,7 @@ Afterwards, every answer keeps its reasoning. Enter on a result opens it with th
|
|
|
50
50
|
- **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.
|
|
51
51
|
- **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.
|
|
52
52
|
- **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.
|
|
53
|
-
- **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.
|
|
53
|
+
- **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.
|
|
54
54
|
- **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.
|
|
55
55
|
- **History.** Verdict changes are appended to `results/verdicts.jsonl`, and replaced results are moved to `history/`, never overwritten.
|
|
56
56
|
|
|
@@ -69,9 +69,21 @@ The best server flags differ per model and per machine, so tuieval splits them b
|
|
|
69
69
|
|
|
70
70
|
## Machines
|
|
71
71
|
|
|
72
|
-
- **Machine id** is detected automatically (e.g. `m3max-64gb`, `m2-16gb
|
|
73
|
-
- **Fit check (
|
|
74
|
-
- **Per-machine settings** go under `[machines.<id>]`: `memory_headroom_gb` (GPU memory kept free
|
|
72
|
+
- **Machine id** is detected automatically (e.g. `m3max-64gb`, `m2-16gb`, or `rtx4090-128gb` on a Linux machine with a discrete GPU; set `EVALS_MACHINE` to rename it). Every result records the machine, the server version and the exact speed flags used. `tuieval machines` lists this machine and every other machine that has run the evals (they record themselves in `tuning/`).
|
|
73
|
+
- **Fit check (llama):** before starting a GGUF, tuieval reads its header (layers, KV heads, hybrid attention layers) and picks the largest context that fits this machine's GPU memory (a Mac's unified memory, or a discrete GPU's VRAM), capped at `max_ctx`. A model that can't fit at 8k context is skipped with *doesn't fit on <machine>* instead of swapping. Packs that need more context than fits are skipped. It never quantizes the KV cache on its own, since that changes answers. Servers that size their own memory (their `cmd` doesn't take `{ctx}`) are left alone: their context is the model's `max_context`; `fit_check` on the server changes that.
|
|
74
|
+
- **Per-machine settings** go under `[machines.<id>]`: `memory_headroom_gb` (GPU memory kept free: default 4 on a Mac, 1.5 in a discrete GPU's VRAM), `gpu_memory_gb` (the GPU memory to size models against, when detection gets it wrong or you want to hold some back), `gpu_residency_gb` (see the stall guard) and `parallel_models` (see below).
|
|
75
|
+
|
|
76
|
+
### Discrete GPUs (Linux: Nvidia, AMD)
|
|
77
|
+
|
|
78
|
+
With `nvidia-smi` (Nvidia) or `amd-smi` / `rocm-smi` (AMD) installed, tuieval reads each card's name and VRAM. Several cards add up, since llama.cpp splits a model across them. `tuieval doctor` shows what it found.
|
|
79
|
+
|
|
80
|
+
- Models are sized to fit **entirely in VRAM** (servers keep `-ngl 99`); splitting a model between GPU and CPU isn't supported. A model that doesn't fit is skipped with what it needs and what's free.
|
|
81
|
+
- **Memory** in results is the server's peak VRAM: its own processes' on Nvidia, the whole machine's on AMD (its tools don't report it per process reliably, so it's only exact when nothing else uses the GPU).
|
|
82
|
+
- `tuieval tune` rejects flags that leave VRAM nearly full. The macOS memory-pressure checks and the stall guard don't apply.
|
|
83
|
+
- A machine that recorded itself before its GPU was detected keeps that id, so its results and tuning still match; `tuieval doctor` names the new id if you want to switch (`EVALS_MACHINE`).
|
|
84
|
+
- Without the vendor tools, models are sized against system RAM, as on any other machine; set `gpu_memory_gb` instead.
|
|
85
|
+
|
|
86
|
+
Windows isn't supported (WSL2 works like Linux).
|
|
75
87
|
|
|
76
88
|
## Several models at a time
|
|
77
89
|
|
|
@@ -86,13 +98,13 @@ By default a run serves one model at a time. On a machine with room for more, se
|
|
|
86
98
|
|
|
87
99
|
## PTA index
|
|
88
100
|
|
|
89
|
-
The **PTA index** puts parsimony (tokens), time and accuracy side by side, as a bar for each per model
|
|
101
|
+
The **PTA index** puts parsimony (tokens), time and accuracy side by side, as a bar for each per model, 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
102
|
|
|
91
103
|
- **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
104
|
- **T time**: the total time to answer those questions, as how many times the fastest model's, e.g. `28×` (★ in **time/answer**).
|
|
93
105
|
- **A accuracy**: the share of answers to those questions that passed.
|
|
94
106
|
|
|
95
|
-
The P and T bars
|
|
107
|
+
The P and T bars are as long as the tokens and time used: the model that used the most fills its bar, the others in proportion (at least one block), so **shorter is better**. The A bar is the share of answers right, so **longer is better**; it's full only when every answer passed. Each heading says which way is better.
|
|
96
108
|
|
|
97
109
|
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.
|
|
98
110
|
|
|
@@ -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.dev66'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'dev66')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -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 []}
|
|
@@ -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")
|
|
@@ -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 []})
|
|
@@ -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:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""PTA index: parsimony (tokens), time and accuracy per model, a bar each
|
|
1
|
+
"""PTA index: parsimony (tokens), time and accuracy per model, a bar each.
|
|
2
2
|
|
|
3
3
|
P parsimony (tokens) the tokens (reasoning and answer) used on every question compared (each
|
|
4
4
|
question at its median over repeats), shown as how many times the leanest
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
model's ("28×").
|
|
8
8
|
A accuracy the share of answers to those questions that passed (%).
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
The P and T bars are as long as the tokens and time used (the model that used the most fills its bar,
|
|
11
|
+
the others in proportion, at least one block), so shorter is better; the A bar: longer is better.
|
|
12
|
+
P and T also carry a 0-100 score (100 for the best, POINTS_PER_DOUBLING points less for every
|
|
13
|
+
doubling), used to order models with the same accuracy.
|
|
12
14
|
|
|
13
15
|
Models are compared on the questions all of them answered, so one that skipped some never looks
|
|
14
16
|
faster. Server and connection errors aren't answers. By default a model with fewer than half as many
|
|
@@ -93,9 +95,11 @@ def index(rows, models=None, memory=None):
|
|
|
93
95
|
"most": most}
|
|
94
96
|
|
|
95
97
|
|
|
96
|
-
# (letter, name, what the
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
# (letter, name, which way is better, what the bar measures, the number beside it, colour). P and T bars
|
|
99
|
+
# are the amount itself (tokens, time: shorter is better); the A bar is the share right (longer is better).
|
|
100
|
+
LETTERS = (("P", "parsimony (tokens)", "▼ shorter is better", "tokens", "tokens_x", "cyan"),
|
|
101
|
+
("T", "time", "▼ shorter is better", "total_s", "time_x", "magenta"),
|
|
102
|
+
("A", "accuracy", "▲ longer is better", "A", "A", "green"))
|
|
99
103
|
|
|
100
104
|
|
|
101
105
|
def times(v):
|
|
@@ -103,26 +107,41 @@ def times(v):
|
|
|
103
107
|
return "-" if v is None else f"{v:.1f}×" if v < 9.95 else f"{v:.0f}×"
|
|
104
108
|
|
|
105
109
|
|
|
110
|
+
def _blocks(width, v, most):
|
|
111
|
+
"""How many of width blocks a bar fills: v as a share of most, at least one block when v > 0."""
|
|
112
|
+
if not v or not most:
|
|
113
|
+
return 0
|
|
114
|
+
return max(1, min(width, round(width * v / most)))
|
|
115
|
+
|
|
116
|
+
|
|
106
117
|
def bars(result, width=20, markup=True):
|
|
107
|
-
"""Lines with a row per model and a bar
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
"""Lines with a row per model and a bar each for P, T and A: the at-a-glance view. The P and T bars
|
|
119
|
+
are proportional to tokens and time (the longest is the model that used most; shorter is better),
|
|
120
|
+
with how many times the leanest / fastest model's beside them; the A bar is the share of answers
|
|
121
|
+
right (longer is better, full only when every answer passed). markup=False gives plain text."""
|
|
110
122
|
if not result["models"]:
|
|
111
123
|
return []
|
|
112
124
|
name_w = max(len(x["model"]) for x in result["models"])
|
|
113
|
-
# each column: the bar, a space, "100%" or "9.8×" (
|
|
114
|
-
cell_w = max(width + 9, *(len(
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
# each column: the bar, a space, "100%" or "9.8×" (6), then a gap; at least as wide as its heading
|
|
126
|
+
cell_w = max(width + 9, *(len(t) + 2 for k, name, better, *_ in LETTERS for t in (f"{k} {name}", better)))
|
|
127
|
+
pad = " " * (name_w + 2)
|
|
128
|
+
head = pad + "".join(f"{f'{k} {name}':<{cell_w}}" for k, name, *_ in LETTERS)
|
|
129
|
+
sub = pad + "".join(f"{better:<{cell_w}}" for _, _, better, *_ in LETTERS)
|
|
130
|
+
lines = [f"[b]{head.rstrip()}[/b]" if markup else head.rstrip(), f"[dim]{sub.rstrip()}[/dim]" if markup else sub.rstrip()]
|
|
131
|
+
most = {m: max((x.get(m) or 0 for x in result["models"]), default=0) for _, _, _, m, _, _ in LETTERS}
|
|
117
132
|
for x in result["models"]:
|
|
118
133
|
cells = []
|
|
119
|
-
for k, _, shown, color in LETTERS:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
for k, _, _, measure, shown, color in LETTERS:
|
|
135
|
+
if k == "A":
|
|
136
|
+
v = x["A"]
|
|
137
|
+
full = 0 if v is None else round(width * max(0, min(100, v)) / 100)
|
|
138
|
+
if v is not None and v < 100: # full only when every answer passed
|
|
139
|
+
full = min(full, width - 1)
|
|
140
|
+
num = "-" if v is None else compare.pct(v / 100)
|
|
141
|
+
else:
|
|
142
|
+
full = _blocks(width, x.get(measure), most[measure])
|
|
143
|
+
num = times(x.get(shown))
|
|
124
144
|
bar, rest = "█" * full, "░" * (width - full)
|
|
125
|
-
num = (("-" if v is None else compare.pct(v / 100)) if shown == "A" else times(x.get(shown)))
|
|
126
145
|
bar = f"[{color}]{bar}[/][grey37]{rest}[/]" if markup else bar + rest
|
|
127
146
|
cells.append(f"{bar} {num:>6}" + " " * (cell_w - width - 7))
|
|
128
147
|
name = f"{x['model']:<{name_w}}"
|
|
@@ -183,8 +202,9 @@ def scope_note(result, models):
|
|
|
183
202
|
if not result["questions"]:
|
|
184
203
|
return "These models have no question in common yet, so there's nothing to compare."
|
|
185
204
|
return (f"Compared on the {result['questions']} questions all {len(result['models'])} model(s) answered. "
|
|
186
|
-
"
|
|
187
|
-
"model's (1.0×, ★ in the table). A accuracy: answers right
|
|
205
|
+
"P parsimony (tokens) and T time: bars as long as the tokens / time used (shorter is better), "
|
|
206
|
+
"and how many times the leanest / fastest model's (1.0×, ★ in the table). A accuracy: answers right "
|
|
207
|
+
"(longer is better).")
|
|
188
208
|
|
|
189
209
|
|
|
190
210
|
def for_engine(e, labels=None, packs=None, results_dir=None):
|
|
@@ -332,9 +332,10 @@ with the model's reasoning and answer as they stream, then recent results with t
|
|
|
332
332
|
v with several models at a time: stream the next one (k skips the one streaming)""",
|
|
333
333
|
"ResultsScreen": """[b]Results[/b]
|
|
334
334
|
|
|
335
|
-
PTA index parsimony (tokens), time and accuracy per model, a bar each
|
|
336
|
-
2.4× = 2.4 times the best),
|
|
337
|
-
|
|
335
|
+
PTA index parsimony (tokens), time and accuracy per model, a bar each: tokens and time
|
|
336
|
+
as long as they took (shorter is better; 2.4× = 2.4 times the best), accuracy
|
|
337
|
+
longer is better. Per-answer numbers, tok/s, TTFT and memory in the table.
|
|
338
|
+
Results open here.
|
|
338
339
|
Production readiness one verdict per model and use case; the table says why and what to run next
|
|
339
340
|
Per question every question, model by model (filters: pack, level, Models disagree)
|
|
340
341
|
Failures every wrong answer with the grader's reason; enter opens it in full
|
|
@@ -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 "
|
|
@@ -761,10 +761,14 @@ class PTAIndex(unittest.TestCase):
|
|
|
761
761
|
self.assertEqual((cells["slow"][1], cells["fast"][1:3]), ("10 ★", ["40", "2.0s ★"])) # per answer
|
|
762
762
|
bars = pta.bars(res, 10, markup=False)
|
|
763
763
|
self.assertEqual(bars[0].split(), ["P", "parsimony", "(tokens)", "T", "time", "A", "accuracy"])
|
|
764
|
+
self.assertEqual(bars[1].split(), ["▼", "shorter", "is", "better"] * 2 + ["▲", "longer", "is", "better"])
|
|
765
|
+
slow, fast = bars[2], bars[3]
|
|
764
766
|
self.assertEqual((bars[0].index("P parsimony"), bars[0].index("T time")), # each bar starts under
|
|
765
|
-
(
|
|
766
|
-
#
|
|
767
|
-
|
|
767
|
+
(fast.index("██████████"), fast.index("███░░░░░░░"))) # its heading
|
|
768
|
+
# P and T bars as long as tokens and time (shorter is better): fast used 4x the tokens of slow but
|
|
769
|
+
# took 4 s against 13 s, so it has the longest token bar and the shortest time bar
|
|
770
|
+
self.assertEqual(fast.split(), ["fast", "██████████", "4.0×", "███░░░░░░░", "1.0×", "█████░░░░░", "50.0%"])
|
|
771
|
+
self.assertEqual(slow.split()[1:5], ["██░░░░░░░░", "1.0×", "██████████", "3.2×"])
|
|
768
772
|
self.assertEqual((pta.times(1), pta.times(9.84), pta.times(27.9)), ("1.0×", "9.8×", "28×"))
|
|
769
773
|
self.assertEqual((by["fast"]["A"], by["slow"]["A"]), (50, 100))
|
|
770
774
|
self.assertEqual([x["model"] for x in res["models"]], ["slow", "fast"]) # best accuracy first
|
|
@@ -774,7 +778,7 @@ class PTAIndex(unittest.TestCase):
|
|
|
774
778
|
self.assertEqual([compare.pct(f) for f in (467 / 469, 1, 0, 0.5)], ["99.6%", "100%", "0%", "50.0%"])
|
|
775
779
|
row = lambda model, a: {"model": model, "P": 100, "T": 100, "A": a, "tokens_x": 1, "time_x": 1}
|
|
776
780
|
bars = pta.bars({"models": [row("two-wrong", 100 * 467 / 469), row("perfect", 100.0)]}, 10, markup=False)
|
|
777
|
-
two_wrong, perfect = bars[
|
|
781
|
+
two_wrong, perfect = bars[2].split(), bars[3].split()
|
|
778
782
|
self.assertEqual((two_wrong[-2], two_wrong[-1]), ("█████████░", "99.6%")) # not a full bar
|
|
779
783
|
self.assertEqual((perfect[-2], perfect[-1]), ("██████████", "100%"))
|
|
780
784
|
|
|
@@ -902,6 +906,153 @@ class Unavailable(unittest.TestCase):
|
|
|
902
906
|
self.assertIn("works", tuieval(ws, "pta").stdout)
|
|
903
907
|
|
|
904
908
|
|
|
909
|
+
class DiscreteGPU(unittest.TestCase):
|
|
910
|
+
"""Nvidia and AMD cards on Linux, read from their vendors' tools (faked here, so no GPU is needed)."""
|
|
911
|
+
|
|
912
|
+
def setUp(self):
|
|
913
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
914
|
+
self.bin = os.path.join(self.tmp.name, "bin")
|
|
915
|
+
os.makedirs(self.bin)
|
|
916
|
+
self.path = os.environ["PATH"]
|
|
917
|
+
os.environ["PATH"] = self.bin + os.pathsep + "/usr/bin:/bin" # only the fakes, never a real GPU tool
|
|
918
|
+
|
|
919
|
+
def tearDown(self):
|
|
920
|
+
os.environ["PATH"] = self.path
|
|
921
|
+
self.tmp.cleanup()
|
|
922
|
+
|
|
923
|
+
def tool(self, name, script):
|
|
924
|
+
path = os.path.join(self.bin, name)
|
|
925
|
+
write(path, "#!/bin/sh\n" + script)
|
|
926
|
+
os.chmod(path, 0o755)
|
|
927
|
+
|
|
928
|
+
def test_nvidia(self):
|
|
929
|
+
from tuieval import machines
|
|
930
|
+
self.tool("nvidia-smi", textwrap.dedent("""
|
|
931
|
+
case "$*" in
|
|
932
|
+
*query-gpu=name,memory.total*) printf 'NVIDIA GeForce RTX 4090, 24564\nNVIDIA GeForce RTX 4090, 24564\n' ;;
|
|
933
|
+
*query-compute-apps*) printf '4242, 18000\n999, 3000\n' ;;
|
|
934
|
+
*query-gpu=memory.used*) printf '18500\n200\n' ;;
|
|
935
|
+
esac
|
|
936
|
+
"""))
|
|
937
|
+
gpus = machines.discrete_gpus()
|
|
938
|
+
self.assertEqual([n for n, _ in gpus], ["NVIDIA GeForce RTX 4090"] * 2)
|
|
939
|
+
self.assertAlmostEqual(sum(g for _, g in gpus), 47.98, places=1)
|
|
940
|
+
self.assertEqual(machines.gpu_id(gpus, 127.6), "2xrtx4090-128gb")
|
|
941
|
+
self.assertAlmostEqual(machines.vram_used_gb([4242, 4243]), 18000 / 1024) # the server's own
|
|
942
|
+
self.assertAlmostEqual(machines.vram_used_gb(), 18700 / 1024) # the whole machine
|
|
943
|
+
|
|
944
|
+
def test_amd(self):
|
|
945
|
+
from tuieval import machines
|
|
946
|
+
self.tool("amd-smi", "exit 1\n") # older ROCm: rocm-smi
|
|
947
|
+
self.tool("rocm-smi", textwrap.dedent("""
|
|
948
|
+
case "$*" in
|
|
949
|
+
*showproductname*) echo '{"card0": {"VRAM Total Memory (B)": "25753026560", "Card Series": "Radeon RX 7900 XTX"}, "system": {}}' ;;
|
|
950
|
+
*) echo '{"card0": {"VRAM Total Used Memory (B)": "4294967296"}}' ;;
|
|
951
|
+
esac
|
|
952
|
+
"""))
|
|
953
|
+
self.assertEqual(machines.discrete_gpus(), [("Radeon RX 7900 XTX", 25753026560 / 2**30)])
|
|
954
|
+
self.assertEqual(machines.gpu_id(machines.discrete_gpus(), 64), "rx7900xtx-64gb")
|
|
955
|
+
self.assertAlmostEqual(machines.vram_used_gb([1]), 4.0) # machine-wide on AMD
|
|
956
|
+
self.tool("amd-smi", "echo '[{\"gpu\": 0, \"asic\": {\"market_name\": \"Radeon PRO W7900\"}, "
|
|
957
|
+
"\"vram\": {\"size\": {\"value\": 49152, \"unit\": \"MB\"}}}]'\n")
|
|
958
|
+
self.assertEqual(machines.discrete_gpus(), [("Radeon PRO W7900", 48.0)])
|
|
959
|
+
|
|
960
|
+
def test_missing_or_broken_tools_mean_no_gpu(self):
|
|
961
|
+
from tuieval import machines
|
|
962
|
+
self.assertEqual(machines.discrete_gpus(), [])
|
|
963
|
+
self.assertIsNone(machines.vram_used_gb([1]))
|
|
964
|
+
self.tool("nvidia-smi", "echo 'NVIDIA-SMI has failed because it could not communicate with the driver'\nexit 9\n")
|
|
965
|
+
self.tool("rocm-smi", "echo 'not json'\n")
|
|
966
|
+
self.assertEqual(machines.discrete_gpus(), [])
|
|
967
|
+
|
|
968
|
+
def test_sizing_ids_and_overrides(self):
|
|
969
|
+
from tuieval import engine, machines
|
|
970
|
+
gpu = machines.Machine("rtx4090-128gb", "x86_64", 128.0, 24.0, 16, 0, None, "Linux",
|
|
971
|
+
gpu_name="NVIDIA GeForce RTX 4090", discrete=True)
|
|
972
|
+
self.assertIn("NVIDIA GeForce RTX 4090 · 24 GB VRAM", gpu.summary)
|
|
973
|
+
ws = os.path.join(self.tmp.name, "ws")
|
|
974
|
+
tuieval(self.tmp.name, "init", ws)
|
|
975
|
+
real = machines.detect
|
|
976
|
+
machines.detect = lambda: gpu
|
|
977
|
+
try:
|
|
978
|
+
e = engine.Engine(root=ws)
|
|
979
|
+
self.assertEqual((e.machine().id, e.headroom_gb(e.machine())), ("rtx4090-128gb", 1.5))
|
|
980
|
+
self.assertEqual(e.memory_available_gb(), 22.5) # VRAM, not 128 GB of RAM
|
|
981
|
+
os.makedirs(os.path.join(ws, "tuning", "x8664-128gb")) # recorded before detection
|
|
982
|
+
self.assertEqual(e.machine().id, "x8664-128gb") # keeps its results' id
|
|
983
|
+
with open(os.path.join(ws, "models.toml"), "a") as f:
|
|
984
|
+
f.write("\n[machines.x8664-128gb]\ngpu_memory_gb = 20\nmemory_headroom_gb = 2\n")
|
|
985
|
+
e = engine.Engine(root=ws)
|
|
986
|
+
self.assertEqual((e.machine().gpu_limit_gb, e.memory_available_gb()), (20.0, 18.0))
|
|
987
|
+
finally:
|
|
988
|
+
machines.detect = real
|
|
989
|
+
def test_doctor_and_vram_peak(self):
|
|
990
|
+
import contextlib, io, threading
|
|
991
|
+
from tuieval import doctor, engine, machines
|
|
992
|
+
self.tool("nvidia-smi", "printf 'NVIDIA GeForce RTX 4090, 24564\\n'\n")
|
|
993
|
+
gpu = machines.Machine("x8664-128gb", "x86_64", 128.0, 24.0, 16, 0, None, "Linux",
|
|
994
|
+
gpu_name="NVIDIA GeForce RTX 4090", discrete=True)
|
|
995
|
+
ws = os.path.join(self.tmp.name, "ws")
|
|
996
|
+
tuieval(self.tmp.name, "init", ws)
|
|
997
|
+
real, real_vram = machines.detect, machines.vram_used_gb
|
|
998
|
+
machines.detect = lambda: gpu
|
|
999
|
+
env = dict(os.environ)
|
|
1000
|
+
os.environ.update(TUIEVAL_HOME=ws, TUIEVAL_DETECT_PORTS="", NO_COLOR="1")
|
|
1001
|
+
try:
|
|
1002
|
+
out = io.StringIO()
|
|
1003
|
+
with contextlib.redirect_stdout(out):
|
|
1004
|
+
doctor.check()
|
|
1005
|
+
text = out.getvalue()
|
|
1006
|
+
self.assertIn("NVIDIA GeForce RTX 4090 · 24 GB VRAM", text)
|
|
1007
|
+
self.assertIn("sized to 24 GB of VRAM, keeping 1.5 GB free", text)
|
|
1008
|
+
self.assertIn("EVALS_MACHINE=rtx4090-128gb", text) # an id kept from before
|
|
1009
|
+
machines.vram_used_gb = lambda pids=None: 12.5
|
|
1010
|
+
e, info, stop = engine.Engine(root=ws), {}, threading.Event()
|
|
1011
|
+
proc = subprocess.Popen(["sleep", "5"])
|
|
1012
|
+
try:
|
|
1013
|
+
e._watch_memory(proc, "m", stop, [0.0], info)
|
|
1014
|
+
time.sleep(1)
|
|
1015
|
+
finally:
|
|
1016
|
+
stop.set()
|
|
1017
|
+
proc.kill()
|
|
1018
|
+
proc.wait()
|
|
1019
|
+
self.assertEqual(info.get("vram_peak_gb"), 12.5)
|
|
1020
|
+
finally:
|
|
1021
|
+
machines.detect, machines.vram_used_gb = real, real_vram
|
|
1022
|
+
os.environ.clear()
|
|
1023
|
+
os.environ.update(env)
|
|
1024
|
+
|
|
1025
|
+
def test_fit_messages_name_vram(self):
|
|
1026
|
+
from tuieval import machines
|
|
1027
|
+
path = os.path.join(self.tmp.name, "m.gguf")
|
|
1028
|
+
fake_gguf(path, size=1000)
|
|
1029
|
+
gpu = lambda gb: machines.Machine("rtx4090-128gb", "x86_64", 128.0, gb, 16, 0, None, "Linux",
|
|
1030
|
+
gpu_name="NVIDIA GeForce RTX 4090", discrete=True)
|
|
1031
|
+
ok, no = machines.fit(path, gpu(24.0), headroom_gb=1.5), machines.fit(path, gpu(1.2), headroom_gb=0.1)
|
|
1032
|
+
self.assertTrue(ok.fits and ok.note.endswith("GB of VRAM)"), ok.note)
|
|
1033
|
+
self.assertTrue(not no.fits and no.note.endswith("GB of VRAM available"), no.note)
|
|
1034
|
+
mac = machines.Machine("m3max-64gb", "Apple M3 Max", 64.0, 48.0, 12, 4, 40, "macOS")
|
|
1035
|
+
self.assertNotIn("VRAM", machines.fit(path, mac).note)
|
|
1036
|
+
|
|
1037
|
+
def test_vram_peak_reaches_the_pta_index(self):
|
|
1038
|
+
from tuieval import engine, pta
|
|
1039
|
+
sittings = [{"started": 1, "wall_s": 10, "vram_peak_gb": 18.2, "peak_rss_mb": 2048},
|
|
1040
|
+
{"started": 2, "wall_s": 10, "vram_peak_gb": 21.5, "peak_rss_mb": 1900}]
|
|
1041
|
+
run = engine.Engine.sittings_summary(sittings)
|
|
1042
|
+
self.assertEqual(run["vram_peak_gb"], 21.5) # the peak over every sitting is kept
|
|
1043
|
+
infos = [{"label": "gpu", "vram_peak_gb": run["vram_peak_gb"], "peak_rss_mb": run["peak_rss_mb"]},
|
|
1044
|
+
{"label": "mac", "peak_rss_mb": 25600}]
|
|
1045
|
+
self.assertEqual(pta.memory_gb(infos), {"gpu": 21.5, "mac": 25.0}) # VRAM where it was measured
|
|
1046
|
+
|
|
1047
|
+
def test_mac_is_unchanged(self):
|
|
1048
|
+
from tuieval import engine, machines
|
|
1049
|
+
e = engine.Engine.__new__(engine.Engine)
|
|
1050
|
+
e.cfg = {"machines": {}}
|
|
1051
|
+
mac = machines.Machine("m3max-64gb", "Apple M3 Max", 64.0, 48.0, 12, 4, 40, "macOS")
|
|
1052
|
+
self.assertFalse(mac.discrete) # Macs are unchanged
|
|
1053
|
+
self.assertEqual(e.headroom_gb(mac), 4.0)
|
|
1054
|
+
|
|
1055
|
+
|
|
905
1056
|
class FirstRun(unittest.TestCase):
|
|
906
1057
|
"""A new user's first run fails fast with the fix, never hangs or shows a bare Python error."""
|
|
907
1058
|
|
|
@@ -1201,7 +1352,7 @@ class WarmTune(unittest.TestCase):
|
|
|
1201
1352
|
fake_gguf(f"{d}/new.gguf", size=1000)
|
|
1202
1353
|
fake_gguf(f"{d}/sib.gguf", size=1100)
|
|
1203
1354
|
fake_gguf(f"{d}/other.gguf", embedding=4096, size=1000)
|
|
1204
|
-
machine = types.SimpleNamespace(id="mac", summary="a Mac")
|
|
1355
|
+
machine = types.SimpleNamespace(id="mac", summary="a Mac", discrete=False)
|
|
1205
1356
|
eng = types.SimpleNamespace(
|
|
1206
1357
|
cfg={"servers": {"llama": {"cmd": ["llama"], "tune": self.KNOBS}},
|
|
1207
1358
|
"models": [{"label": n, "server": "llama", "model": f"{d}/{n}.gguf"}
|
|
@@ -1212,6 +1363,7 @@ class WarmTune(unittest.TestCase):
|
|
|
1212
1363
|
eng.serving = lambda m: types.SimpleNamespace(fits=True, ctx=8192, machine=machine, mtp_layers=1,
|
|
1213
1364
|
identity={"model_bytes": 1000})
|
|
1214
1365
|
eng.knob_values = lambda mc: {}
|
|
1366
|
+
eng.machine = lambda: machine
|
|
1215
1367
|
eng.server_version = lambda m: "v1"
|
|
1216
1368
|
eng.gpu_residency_gb = lambda: 24
|
|
1217
1369
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/tool_call/system.txt
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/tool_call/tests.yaml
RENAMED
|
File without changes
|
{tuieval-0.2.0.dev62 → tuieval-0.2.0.dev66}/src/tuieval/templates/packs/tool_call/tools.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|