benchkit-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- benchkit/__init__.py +1 -0
- benchkit/_pi_proxy.py +219 -0
- benchkit/answer_key_guard.ts +49 -0
- benchkit/artifacts.py +87 -0
- benchkit/benchmarks/__init__.py +105 -0
- benchkit/benchmarks/aider_polyglot.py +405 -0
- benchkit/benchmarks/arc.py +52 -0
- benchkit/benchmarks/banking77.py +149 -0
- benchkit/benchmarks/base.py +19 -0
- benchkit/benchmarks/boolq.py +49 -0
- benchkit/benchmarks/chartqa.py +133 -0
- benchkit/benchmarks/code_verifier.py +77 -0
- benchkit/benchmarks/downloads.py +166 -0
- benchkit/benchmarks/evalplus.py +288 -0
- benchkit/benchmarks/git_surgery.py +485 -0
- benchkit/benchmarks/gpqa.py +56 -0
- benchkit/benchmarks/gsm8k.py +97 -0
- benchkit/benchmarks/hellaswag.py +49 -0
- benchkit/benchmarks/humaneval.py +103 -0
- benchkit/benchmarks/ifeval.py +56 -0
- benchkit/benchmarks/ifeval_instructions.py +477 -0
- benchkit/benchmarks/mbpp.py +60 -0
- benchkit/benchmarks/mc_arena.py +479 -0
- benchkit/benchmarks/mcq.py +94 -0
- benchkit/benchmarks/medfailbench.py +537 -0
- benchkit/benchmarks/mmlu.py +54 -0
- benchkit/benchmarks/mmlu_pro.py +62 -0
- benchkit/benchmarks/mmvp.py +115 -0
- benchkit/benchmarks/ocrbench.py +96 -0
- benchkit/benchmarks/openbookqa.py +52 -0
- benchkit/benchmarks/patcheval.py +929 -0
- benchkit/benchmarks/piqa.py +52 -0
- benchkit/benchmarks/ruler.py +172 -0
- benchkit/benchmarks/ruler_generator.py +535 -0
- benchkit/benchmarks/sanity.py +174 -0
- benchkit/benchmarks/treejs_arena.py +268 -0
- benchkit/benchmarks/truthfulqa.py +52 -0
- benchkit/benchmarks/utils.py +25 -0
- benchkit/benchmarks/vision.py +230 -0
- benchkit/benchmarks/vstar.py +108 -0
- benchkit/benchmarks/winogrande.py +52 -0
- benchkit/benchmarks/xstest.py +116 -0
- benchkit/browser.py +429 -0
- benchkit/catalog.py +121 -0
- benchkit/cli.py +1000 -0
- benchkit/client.py +1413 -0
- benchkit/controls.py +87 -0
- benchkit/datasets/MMLU_LICENSE.txt +21 -0
- benchkit/datasets/README.md +142 -0
- benchkit/datasets/arc_challenge.jsonl +1172 -0
- benchkit/datasets/banking77.jsonl +3080 -0
- benchkit/datasets/banking77_intents.json +79 -0
- benchkit/datasets/boolq.jsonl +3270 -0
- benchkit/datasets/code_verifier.jsonl +500 -0
- benchkit/datasets/gpqa_diamond.jsonl +198 -0
- benchkit/datasets/gsm8k.jsonl +1319 -0
- benchkit/datasets/hellaswag.jsonl +1000 -0
- benchkit/datasets/humaneval.jsonl +164 -0
- benchkit/datasets/ifeval.jsonl +541 -0
- benchkit/datasets/mbpp.jsonl +500 -0
- benchkit/datasets/mc_blocks_1_20_1.jsonl +1003 -0
- benchkit/datasets/medfailbench_hard30.jsonl +30 -0
- benchkit/datasets/medfailbench_hard30.md +13 -0
- benchkit/datasets/openbookqa.jsonl +500 -0
- benchkit/datasets/piqa.jsonl +1838 -0
- benchkit/datasets/sanity_code.jsonl +6 -0
- benchkit/datasets/truthfulqa.jsonl +817 -0
- benchkit/datasets/winogrande.jsonl +1267 -0
- benchkit/datasets/xstest.csv +451 -0
- benchkit/decision.py +194 -0
- benchkit/demo.py +451 -0
- benchkit/dockerfiles.py +247 -0
- benchkit/engine.py +1270 -0
- benchkit/evaluation.py +195 -0
- benchkit/events.py +141 -0
- benchkit/executor.py +96 -0
- benchkit/git_surgery/backport-release-stack/setup.sh +328 -0
- benchkit/git_surgery/backport-release-stack/verify.sh +119 -0
- benchkit/git_surgery/bisect-the-regression/setup.sh +49 -0
- benchkit/git_surgery/bisect-the-regression/verify.sh +34 -0
- benchkit/git_surgery/rebase-conflict-chain/setup.sh +65 -0
- benchkit/git_surgery/rebase-conflict-chain/verify.sh +43 -0
- benchkit/git_surgery/recover-complex-stash/setup.sh +152 -0
- benchkit/git_surgery/recover-complex-stash/verify.sh +95 -0
- benchkit/git_surgery/recover-lost-work/setup.sh +40 -0
- benchkit/git_surgery/recover-lost-work/verify.sh +36 -0
- benchkit/git_surgery/repair-force-pushed-remote/setup.sh +171 -0
- benchkit/git_surgery/repair-force-pushed-remote/verify.sh +83 -0
- benchkit/git_surgery/revert-merge-with-followups/setup.sh +198 -0
- benchkit/git_surgery/revert-merge-with-followups/verify.sh +98 -0
- benchkit/git_surgery/secret-in-history/README.md +27 -0
- benchkit/git_surgery/secret-in-history/setup.sh +107 -0
- benchkit/git_surgery/secret-in-history/verify.sh +106 -0
- benchkit/git_surgery/split-the-mega-commit/setup.sh +50 -0
- benchkit/git_surgery/split-the-mega-commit/verify.sh +48 -0
- benchkit/git_surgery/untangle-nested-submodules/setup.sh +193 -0
- benchkit/git_surgery/untangle-nested-submodules/verify.sh +90 -0
- benchkit/history.py +474 -0
- benchkit/jobs.py +332 -0
- benchkit/leakage.py +51 -0
- benchkit/looping.py +873 -0
- benchkit/mc_render.py +274 -0
- benchkit/mc_viewer/dist/atlas.png +0 -0
- benchkit/mc_viewer/dist/blockStates.json.gz +0 -0
- benchkit/mc_viewer/dist/version.json +4 -0
- benchkit/mc_viewer/dist/viewer.html +23 -0
- benchkit/mc_viewer/dist/viewer.js +2 -0
- benchkit/mc_viewer/dist/viewer.js.LICENSE.txt +25 -0
- benchkit/mc_viewer/dist/worker.js +2 -0
- benchkit/mc_viewer/dist/worker.js.LICENSE.txt +19 -0
- benchkit/metrics.py +100 -0
- benchkit/ollama.py +60 -0
- benchkit/perf.py +353 -0
- benchkit/perf_report.py +207 -0
- benchkit/perturbations.py +208 -0
- benchkit/pi_agent.py +476 -0
- benchkit/pi_package/package-lock.json +1827 -0
- benchkit/pi_package/package.json +8 -0
- benchkit/report.py +142 -0
- benchkit/report_markdown.py +645 -0
- benchkit/results.py +688 -0
- benchkit/retry.py +163 -0
- benchkit/runner.py +769 -0
- benchkit/sandbox.py +1167 -0
- benchkit/template_check.py +291 -0
- benchkit/templates/arena.html +504 -0
- benchkit/templates/history.html +58 -0
- benchkit/templates/perf.html +509 -0
- benchkit/templates/report.html +769 -0
- benchkit/tui/__init__.py +5 -0
- benchkit/tui/app.py +87 -0
- benchkit/tui/app.tcss +512 -0
- benchkit/tui/formatting.py +84 -0
- benchkit/tui/screens/__init__.py +1 -0
- benchkit/tui/screens/connect.py +191 -0
- benchkit/tui/screens/detail.py +288 -0
- benchkit/tui/screens/help.py +84 -0
- benchkit/tui/screens/modals.py +544 -0
- benchkit/tui/screens/results.py +361 -0
- benchkit/tui/screens/run.py +988 -0
- benchkit/tui/screens/setup.py +733 -0
- benchkit/tui/theme.py +135 -0
- benchkit/tui/widgets.py +120 -0
- benchkit_cli-0.1.0.dist-info/METADATA +294 -0
- benchkit_cli-0.1.0.dist-info/RECORD +149 -0
- benchkit_cli-0.1.0.dist-info/WHEEL +5 -0
- benchkit_cli-0.1.0.dist-info/entry_points.txt +3 -0
- benchkit_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- benchkit_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* The buffer module from node.js, for the browser.
|
|
3
|
+
*
|
|
4
|
+
* @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
|
|
5
|
+
* @license MIT
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/*!
|
|
9
|
+
* The buffer module from node.js, for the browser.
|
|
10
|
+
*
|
|
11
|
+
* @author Feross Aboukhadijeh <https://feross.org>
|
|
12
|
+
* @license MIT
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
|
16
|
+
|
|
17
|
+
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
|
18
|
+
|
|
19
|
+
/** @license URI.js v4.4.1 (c) 2011 Gary Court. License: http://github.com/garycourt/uri-js */
|
benchkit/metrics.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Throughput metrics shared by execution, front-ends and reports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def throughput_metrics(
|
|
9
|
+
*,
|
|
10
|
+
output_tokens: int,
|
|
11
|
+
generation_time_s: float,
|
|
12
|
+
request_time_s: float,
|
|
13
|
+
wall_time_s: float,
|
|
14
|
+
) -> dict[str, float]:
|
|
15
|
+
"""Calculate stream speed, total throughput and occupied request slots.
|
|
16
|
+
|
|
17
|
+
``generation_time_s`` is the sum of server-reported decode times, while
|
|
18
|
+
``request_time_s`` is the sum of complete per-task request durations.
|
|
19
|
+
Their denominators intentionally differ: stream speed describes decoder
|
|
20
|
+
performance and effective concurrency describes client/server saturation.
|
|
21
|
+
"""
|
|
22
|
+
return {
|
|
23
|
+
"tok_s_per_stream": (
|
|
24
|
+
output_tokens / generation_time_s if generation_time_s > 0 else 0.0
|
|
25
|
+
),
|
|
26
|
+
"tok_s_aggregate": (output_tokens / wall_time_s if wall_time_s > 0 else 0.0),
|
|
27
|
+
"concurrency_eff": (request_time_s / wall_time_s if wall_time_s > 0 else 0.0),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _number(value: object) -> float:
|
|
32
|
+
try:
|
|
33
|
+
return float(value)
|
|
34
|
+
except (TypeError, ValueError):
|
|
35
|
+
return 0.0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def stream_tok_s(result: Mapping[str, object]) -> float:
|
|
39
|
+
"""Read stream throughput from new or legacy result data."""
|
|
40
|
+
if "tok_s_per_stream" in result:
|
|
41
|
+
return _number(result["tok_s_per_stream"])
|
|
42
|
+
return _number(result.get("tok_s"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def effective_concurrency(result: Mapping[str, object]) -> float:
|
|
46
|
+
"""Read or derive occupied request slots from a result."""
|
|
47
|
+
if "concurrency_eff" in result:
|
|
48
|
+
return _number(result["concurrency_eff"])
|
|
49
|
+
wall_time = _number(result.get("total_time"))
|
|
50
|
+
if wall_time <= 0:
|
|
51
|
+
return 0.0
|
|
52
|
+
derived = (
|
|
53
|
+
_number(result.get("avg_response_time"))
|
|
54
|
+
* _number(result.get("total"))
|
|
55
|
+
/ wall_time
|
|
56
|
+
)
|
|
57
|
+
configured = _number(result.get("concurrency"))
|
|
58
|
+
return min(configured, derived) if configured > 0 else derived
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def aggregate_tok_s(result: Mapping[str, object]) -> float:
|
|
62
|
+
"""Read or derive wall-clock output throughput from a result."""
|
|
63
|
+
if "tok_s_aggregate" in result:
|
|
64
|
+
return _number(result["tok_s_aggregate"])
|
|
65
|
+
|
|
66
|
+
wall_time = _number(result.get("total_time"))
|
|
67
|
+
tasks = result.get("tasks")
|
|
68
|
+
if wall_time > 0 and isinstance(tasks, list):
|
|
69
|
+
token_values = [
|
|
70
|
+
_number(task.get("output_tokens"))
|
|
71
|
+
for task in tasks
|
|
72
|
+
if isinstance(task, Mapping) and "output_tokens" in task
|
|
73
|
+
]
|
|
74
|
+
if token_values:
|
|
75
|
+
return sum(token_values) / wall_time
|
|
76
|
+
|
|
77
|
+
# Legacy CSV summaries do not contain tasks. Stream throughput multiplied
|
|
78
|
+
# by measured slot occupancy is the closest derivation available.
|
|
79
|
+
return stream_tok_s(result) * effective_concurrency(result)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_decision(result: Mapping[str, object]) -> bool:
|
|
83
|
+
"""Whether a result came from a native decision model."""
|
|
84
|
+
return result.get("harness") == "decision"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def decision_speed(result: Mapping[str, object]) -> tuple[str, str]:
|
|
88
|
+
"""Decision models emit no tokens, so speed is decisions/s plus latency."""
|
|
89
|
+
return (
|
|
90
|
+
f"{_number(result.get('decisions_per_s')):.1f} dec/s",
|
|
91
|
+
f"p50 {_number(result.get('decision_latency_p50_ms')):.0f} ms · "
|
|
92
|
+
f"p95 {_number(result.get('decision_latency_p95_ms')):.0f} ms",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def latency_text(result: Mapping[str, object]) -> str:
|
|
97
|
+
"""Typical request time: median ms for decisions, mean seconds otherwise."""
|
|
98
|
+
if is_decision(result):
|
|
99
|
+
return f"{_number(result.get('decision_latency_p50_ms')):.0f}ms"
|
|
100
|
+
return f"{result.get('avg_response_time', 0)}s"
|
benchkit/ollama.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Ollama API client."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
DEFAULT_HOST = "http://localhost:11434"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_host() -> str:
|
|
11
|
+
return os.environ.get("OLLAMA_HOST", DEFAULT_HOST).rstrip("/")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def list_models(host: str) -> list[dict]:
|
|
15
|
+
r = httpx.get(f"{host}/api/tags", timeout=10)
|
|
16
|
+
r.raise_for_status()
|
|
17
|
+
models = r.json().get("models", [])
|
|
18
|
+
return sorted(models, key=lambda m: m.get("size", 0))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def unload_model(host: str, model: str) -> None:
|
|
22
|
+
"""Evict model from Ollama VRAM by setting keep_alive to 0."""
|
|
23
|
+
httpx.post(
|
|
24
|
+
f"{host}/api/generate",
|
|
25
|
+
json={"model": model, "keep_alive": 0},
|
|
26
|
+
timeout=30,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def generate(host: str, model: str, prompt: str) -> dict:
|
|
31
|
+
r = httpx.post(
|
|
32
|
+
f"{host}/api/generate",
|
|
33
|
+
json={
|
|
34
|
+
"model": model,
|
|
35
|
+
"prompt": prompt,
|
|
36
|
+
"stream": False,
|
|
37
|
+
"options": {"temperature": 0.0},
|
|
38
|
+
},
|
|
39
|
+
timeout=300,
|
|
40
|
+
)
|
|
41
|
+
r.raise_for_status()
|
|
42
|
+
data = r.json()
|
|
43
|
+
|
|
44
|
+
response = data.get("response", "")
|
|
45
|
+
done_reason = data.get("done_reason", "")
|
|
46
|
+
|
|
47
|
+
eval_count = data.get("eval_count", 0)
|
|
48
|
+
eval_duration_ns = data.get("eval_duration", 0)
|
|
49
|
+
total_duration_ns = data.get("total_duration", 0)
|
|
50
|
+
tok_s = eval_count / (eval_duration_ns / 1e9) if eval_duration_ns > 0 else 0.0
|
|
51
|
+
response_time_s = total_duration_ns / 1e9
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"response": response,
|
|
55
|
+
"tok_s": tok_s,
|
|
56
|
+
"eval_count": eval_count,
|
|
57
|
+
"eval_duration_ns": eval_duration_ns,
|
|
58
|
+
"response_time_s": response_time_s,
|
|
59
|
+
"done_reason": done_reason,
|
|
60
|
+
}
|
benchkit/perf.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Controlled model-speed profiling for llama.cpp-compatible servers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import statistics
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import asdict, dataclass
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from benchkit.client import InferenceClient
|
|
14
|
+
|
|
15
|
+
DEFAULT_DEPTHS = (0, 4096, 16384, 32768, 65536)
|
|
16
|
+
DEPTH_TOLERANCE = 0.01
|
|
17
|
+
DRIFT_TOLERANCE = 0.10
|
|
18
|
+
MIN_RELIABLE_PP_TOKENS = 256
|
|
19
|
+
FILLER = (
|
|
20
|
+
"A local inference server processes this deterministic BenchKit workload. "
|
|
21
|
+
"The passage is intentionally ordinary so tokenization remains stable "
|
|
22
|
+
"across repetitions while context depth changes. "
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class PerfConfig:
|
|
28
|
+
"""Inputs that make a performance profile reproducible."""
|
|
29
|
+
|
|
30
|
+
model: str
|
|
31
|
+
depths: tuple[int, ...] = DEFAULT_DEPTHS
|
|
32
|
+
gen_tokens: int = 128
|
|
33
|
+
reps: int = 5
|
|
34
|
+
warmups: int = 1
|
|
35
|
+
config_note: str = ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_depths(value: str) -> tuple[int, ...]:
|
|
39
|
+
"""Parse comma-separated token depths such as ``minimal,4k,16k``."""
|
|
40
|
+
depths: list[int] = []
|
|
41
|
+
for raw in value.split(","):
|
|
42
|
+
item = raw.strip().lower()
|
|
43
|
+
if item in {"minimal", "min", "0"}:
|
|
44
|
+
depth = 0
|
|
45
|
+
else:
|
|
46
|
+
multiplier = 1
|
|
47
|
+
if item.endswith("k"):
|
|
48
|
+
multiplier, item = 1024, item[:-1]
|
|
49
|
+
elif item.endswith("m"):
|
|
50
|
+
multiplier, item = 1024 * 1024, item[:-1]
|
|
51
|
+
try:
|
|
52
|
+
depth = int(float(item) * multiplier)
|
|
53
|
+
except ValueError as exc:
|
|
54
|
+
raise ValueError(f"Invalid depth: {raw.strip()!r}") from exc
|
|
55
|
+
if depth < 1:
|
|
56
|
+
raise ValueError(f"Invalid depth: {raw.strip()!r}")
|
|
57
|
+
if depth not in depths:
|
|
58
|
+
depths.append(depth)
|
|
59
|
+
if not depths:
|
|
60
|
+
raise ValueError("At least one context depth is required")
|
|
61
|
+
return tuple(depths)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def depth_label(depth: int) -> str:
|
|
65
|
+
if depth == 0:
|
|
66
|
+
return "minimal"
|
|
67
|
+
if depth % 1024 == 0:
|
|
68
|
+
return f"{depth // 1024}k"
|
|
69
|
+
return f"{depth:,}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _filler_text(length: int) -> str:
|
|
73
|
+
repetitions = max(1, length // len(FILLER) + 1)
|
|
74
|
+
return (FILLER * repetitions)[:length]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _prompt_for(
|
|
78
|
+
client: InferenceClient,
|
|
79
|
+
model: str,
|
|
80
|
+
target_depth: int,
|
|
81
|
+
announce: Callable[[str], None],
|
|
82
|
+
) -> tuple[str | list[int], str, list[dict]]:
|
|
83
|
+
if target_depth == 0:
|
|
84
|
+
return (
|
|
85
|
+
"BenchKit speed profile. Continue with concise factual prose:",
|
|
86
|
+
"minimal",
|
|
87
|
+
[],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
content = _filler_text(target_depth * 8)
|
|
91
|
+
tokens = client.tokenize(model, content)
|
|
92
|
+
if tokens is not None:
|
|
93
|
+
while len(tokens) < target_depth:
|
|
94
|
+
content += _filler_text(target_depth * 2)
|
|
95
|
+
tokens = client.tokenize(model, content)
|
|
96
|
+
if tokens is None:
|
|
97
|
+
break
|
|
98
|
+
if tokens is not None and len(tokens) >= target_depth:
|
|
99
|
+
return tokens[:target_depth], "tokenizer", []
|
|
100
|
+
|
|
101
|
+
# llama-swap commonly proxies the OpenAI routes but not llama.cpp's native
|
|
102
|
+
# tokenizer. Calibrate text length against the completion response instead
|
|
103
|
+
# of silently trusting a characters-per-token estimate.
|
|
104
|
+
prompt = _filler_text(target_depth * 5)
|
|
105
|
+
calibration: list[dict] = []
|
|
106
|
+
for attempt in range(3):
|
|
107
|
+
announce(f"{depth_label(target_depth)} · calibrating input {attempt + 1}/3")
|
|
108
|
+
try:
|
|
109
|
+
measured = client.profile_completion(model, prompt, 1)
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
calibration.append(
|
|
112
|
+
{
|
|
113
|
+
"attempt": attempt + 1,
|
|
114
|
+
"characters": len(prompt),
|
|
115
|
+
"error": f"{type(exc).__name__}: {exc}",
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
actual = int(measured.get("input_tokens") or 0)
|
|
121
|
+
calibration.append(
|
|
122
|
+
{
|
|
123
|
+
"attempt": attempt + 1,
|
|
124
|
+
"characters": len(prompt),
|
|
125
|
+
"actual_input_tokens": actual,
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
if not actual:
|
|
129
|
+
break
|
|
130
|
+
error_ratio = abs(actual - target_depth) / target_depth
|
|
131
|
+
if error_ratio <= DEPTH_TOLERANCE:
|
|
132
|
+
return prompt, "calibrated", calibration
|
|
133
|
+
prompt = _filler_text(max(1, round(len(prompt) * target_depth / actual)))
|
|
134
|
+
|
|
135
|
+
calibrated = any(item.get("actual_input_tokens") for item in calibration)
|
|
136
|
+
return prompt, "calibrated" if calibrated else "estimated", calibration
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _summary(values: list[float]) -> dict[str, float]:
|
|
140
|
+
if not values:
|
|
141
|
+
return {"median": 0.0, "min": 0.0, "max": 0.0}
|
|
142
|
+
return {
|
|
143
|
+
"median": round(statistics.median(values), 3),
|
|
144
|
+
"min": round(min(values), 3),
|
|
145
|
+
"max": round(max(values), 3),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _summarize_case(
|
|
150
|
+
requested_depth: int,
|
|
151
|
+
depth_method: str,
|
|
152
|
+
calibration: list[dict],
|
|
153
|
+
runs: list[dict],
|
|
154
|
+
errors: list[str],
|
|
155
|
+
) -> dict:
|
|
156
|
+
successful = [run for run in runs if not run.get("error")]
|
|
157
|
+
actual_depths = [run["input_tokens"] for run in successful if run["input_tokens"]]
|
|
158
|
+
actual_depth = int(statistics.median(actual_depths)) if actual_depths else 0
|
|
159
|
+
depth_deviation = (
|
|
160
|
+
(actual_depth - requested_depth) / requested_depth
|
|
161
|
+
if requested_depth and actual_depth
|
|
162
|
+
else 0.0
|
|
163
|
+
)
|
|
164
|
+
return {
|
|
165
|
+
"depth": requested_depth,
|
|
166
|
+
"depth_label": depth_label(requested_depth),
|
|
167
|
+
"actual_input_tokens": actual_depth,
|
|
168
|
+
"depth_method": depth_method,
|
|
169
|
+
"exact_depth": depth_method == "tokenizer",
|
|
170
|
+
"depth_deviation": round(depth_deviation, 5),
|
|
171
|
+
"depth_within_tolerance": (
|
|
172
|
+
requested_depth == 0
|
|
173
|
+
or (actual_depth > 0 and abs(depth_deviation) <= DEPTH_TOLERANCE)
|
|
174
|
+
),
|
|
175
|
+
"calibration": calibration,
|
|
176
|
+
"pp_reliable": actual_depth >= MIN_RELIABLE_PP_TOKENS,
|
|
177
|
+
"successful_reps": len(successful),
|
|
178
|
+
"errors": errors,
|
|
179
|
+
"pp_tps": _summary([run["pp_tps"] for run in successful]),
|
|
180
|
+
"tg_tps": _summary([run["tg_tps"] for run in successful]),
|
|
181
|
+
"ttft_s": _summary([run["ttft_s"] for run in successful]),
|
|
182
|
+
"wall_time_s": _summary([run["wall_time_s"] for run in successful]),
|
|
183
|
+
"wall_tg_tps": _summary([run["wall_tg_tps"] for run in successful]),
|
|
184
|
+
"overhead_available": any(
|
|
185
|
+
run.get("server_time_s", 0) > 0 for run in successful
|
|
186
|
+
),
|
|
187
|
+
"overhead_s": _summary([run["overhead_s"] for run in successful]),
|
|
188
|
+
"output_tokens": _summary([float(run["output_tokens"]) for run in successful]),
|
|
189
|
+
"runs": runs,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def run_profile(
|
|
194
|
+
client: InferenceClient,
|
|
195
|
+
config: PerfConfig,
|
|
196
|
+
*,
|
|
197
|
+
model_meta: dict | None = None,
|
|
198
|
+
progress: Callable[[str], None] | None = None,
|
|
199
|
+
) -> dict:
|
|
200
|
+
"""Run a serial context-depth sweep and return a complete raw profile."""
|
|
201
|
+
|
|
202
|
+
def announce(message: str) -> None:
|
|
203
|
+
if progress is not None:
|
|
204
|
+
progress(message)
|
|
205
|
+
|
|
206
|
+
cases: list[dict] = []
|
|
207
|
+
startup: dict | None = None
|
|
208
|
+
first_prompt: str | list[int] | None = None
|
|
209
|
+
|
|
210
|
+
# Run this before tokenization because llama-swap may need to load the model
|
|
211
|
+
# even for its native tokenizer endpoint.
|
|
212
|
+
if config.warmups:
|
|
213
|
+
announce("First request · startup probe")
|
|
214
|
+
try:
|
|
215
|
+
measured = client.profile_completion(
|
|
216
|
+
config.model,
|
|
217
|
+
"BenchKit startup probe. Continue with concise factual prose:",
|
|
218
|
+
min(config.gen_tokens, 32),
|
|
219
|
+
)
|
|
220
|
+
startup = {
|
|
221
|
+
"ttft_s": round(measured["ttft_s"], 3),
|
|
222
|
+
"wall_time_s": round(measured["wall_time_s"], 3),
|
|
223
|
+
"label": "may include llama-swap model loading",
|
|
224
|
+
}
|
|
225
|
+
except Exception as exc:
|
|
226
|
+
startup = {
|
|
227
|
+
"error": f"{type(exc).__name__}: {exc}",
|
|
228
|
+
"label": "first request",
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for depth in config.depths:
|
|
232
|
+
label = depth_label(depth)
|
|
233
|
+
announce(f"Preparing {label} context")
|
|
234
|
+
prompt, depth_method, calibration = _prompt_for(
|
|
235
|
+
client, config.model, depth, announce
|
|
236
|
+
)
|
|
237
|
+
if first_prompt is None:
|
|
238
|
+
first_prompt = prompt
|
|
239
|
+
|
|
240
|
+
for warmup in range(config.warmups):
|
|
241
|
+
announce(f"{label} · warmup {warmup + 1}/{config.warmups}")
|
|
242
|
+
with contextlib.suppress(Exception):
|
|
243
|
+
client.profile_completion(
|
|
244
|
+
config.model, prompt, min(config.gen_tokens, 32)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
runs: list[dict] = []
|
|
248
|
+
errors: list[str] = []
|
|
249
|
+
for repetition in range(config.reps):
|
|
250
|
+
announce(f"{label} · run {repetition + 1}/{config.reps}")
|
|
251
|
+
try:
|
|
252
|
+
measured = client.profile_completion(
|
|
253
|
+
config.model, prompt, config.gen_tokens
|
|
254
|
+
)
|
|
255
|
+
runs.append(
|
|
256
|
+
{
|
|
257
|
+
"repetition": repetition + 1,
|
|
258
|
+
**{
|
|
259
|
+
key: round(value, 4) if isinstance(value, float) else value
|
|
260
|
+
for key, value in measured.items()
|
|
261
|
+
if key != "response"
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
except Exception as exc:
|
|
266
|
+
error = f"{type(exc).__name__}: {exc}"
|
|
267
|
+
errors.append(error)
|
|
268
|
+
runs.append({"repetition": repetition + 1, "error": error})
|
|
269
|
+
|
|
270
|
+
cases.append(
|
|
271
|
+
_summarize_case(
|
|
272
|
+
depth,
|
|
273
|
+
depth_method,
|
|
274
|
+
calibration,
|
|
275
|
+
runs,
|
|
276
|
+
errors,
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
drift_check: dict | None = None
|
|
281
|
+
if first_prompt is not None and cases:
|
|
282
|
+
announce(f"{cases[0]['depth_label']} · end-of-sweep drift check")
|
|
283
|
+
try:
|
|
284
|
+
measured = client.profile_completion(
|
|
285
|
+
config.model, first_prompt, config.gen_tokens
|
|
286
|
+
)
|
|
287
|
+
initial_tg = float(cases[0]["tg_tps"]["median"])
|
|
288
|
+
final_tg = float(measured["tg_tps"])
|
|
289
|
+
change = (final_tg - initial_tg) / initial_tg if initial_tg else 0.0
|
|
290
|
+
drift_check = {
|
|
291
|
+
"depth": cases[0]["depth"],
|
|
292
|
+
"depth_label": cases[0]["depth_label"],
|
|
293
|
+
"initial_tg_tps": round(initial_tg, 3),
|
|
294
|
+
"final_tg_tps": round(final_tg, 3),
|
|
295
|
+
"change": round(change, 5),
|
|
296
|
+
"stable": bool(initial_tg) and abs(change) <= DRIFT_TOLERANCE,
|
|
297
|
+
"tolerance": DRIFT_TOLERANCE,
|
|
298
|
+
}
|
|
299
|
+
except Exception as exc:
|
|
300
|
+
drift_check = {
|
|
301
|
+
"depth": cases[0]["depth"],
|
|
302
|
+
"depth_label": cases[0]["depth_label"],
|
|
303
|
+
"error": f"{type(exc).__name__}: {exc}",
|
|
304
|
+
"stable": False,
|
|
305
|
+
"tolerance": DRIFT_TOLERANCE,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
tg_depth_medians = [
|
|
309
|
+
float(case["tg_tps"]["median"])
|
|
310
|
+
for case in cases
|
|
311
|
+
if case["successful_reps"] and case["tg_tps"]["median"] > 0
|
|
312
|
+
]
|
|
313
|
+
pp_depth_medians = [
|
|
314
|
+
float(case["pp_tps"]["median"])
|
|
315
|
+
for case in cases
|
|
316
|
+
if (
|
|
317
|
+
case["successful_reps"]
|
|
318
|
+
and case["pp_reliable"]
|
|
319
|
+
and case["pp_tps"]["median"] > 0
|
|
320
|
+
)
|
|
321
|
+
]
|
|
322
|
+
sweep_summary = {
|
|
323
|
+
"tg_tps_median": round(statistics.median(tg_depth_medians), 3)
|
|
324
|
+
if tg_depth_medians
|
|
325
|
+
else 0.0,
|
|
326
|
+
"pp_tps_median": round(statistics.median(pp_depth_medians), 3)
|
|
327
|
+
if pp_depth_medians
|
|
328
|
+
else 0.0,
|
|
329
|
+
"tg_depths": len(tg_depth_medians),
|
|
330
|
+
"pp_depths": len(pp_depth_medians),
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
"kind": "benchkit-performance-profile",
|
|
335
|
+
"version": 1,
|
|
336
|
+
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
337
|
+
"model": config.model,
|
|
338
|
+
"provider": client.label,
|
|
339
|
+
"host": client.host,
|
|
340
|
+
"environment": {
|
|
341
|
+
"hardware": os.environ.get("BENCHKIT_HARDWARE", ""),
|
|
342
|
+
"system": platform.system(),
|
|
343
|
+
"machine": platform.machine(),
|
|
344
|
+
"config_note": config.config_note
|
|
345
|
+
or os.environ.get("BENCHKIT_PERF_CONFIG", ""),
|
|
346
|
+
},
|
|
347
|
+
"model_metadata": model_meta or {},
|
|
348
|
+
"settings": asdict(config),
|
|
349
|
+
"startup": startup,
|
|
350
|
+
"drift_check": drift_check,
|
|
351
|
+
"sweep_summary": sweep_summary,
|
|
352
|
+
"cases": cases,
|
|
353
|
+
}
|