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,133 @@
|
|
|
1
|
+
"""ChartQA - questions about bar, line and pie charts (2,500 test images).
|
|
2
|
+
|
|
3
|
+
Half the questions are human-written and need reasoning across the chart;
|
|
4
|
+
half are machine-generated lookups. Scoring is the standard relaxed accuracy:
|
|
5
|
+
a numeric answer passes within 5% of the reference, anything else must match
|
|
6
|
+
exactly up to case. Slices interleave the two halves.
|
|
7
|
+
|
|
8
|
+
One deliberate leniency over the reference scorer: no test label carries a
|
|
9
|
+
"%", yet chat models write "6.8%" for a label of 6.8. A percent answer is
|
|
10
|
+
therefore read both as written and divided by 100, and passes if either fits.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from functools import cache
|
|
15
|
+
|
|
16
|
+
from benchkit.benchmarks.base import Task
|
|
17
|
+
from benchkit.benchmarks.utils import strip_think_tags
|
|
18
|
+
from benchkit.benchmarks.vision import (
|
|
19
|
+
category_scores,
|
|
20
|
+
load_rows,
|
|
21
|
+
spread,
|
|
22
|
+
vision_dataset,
|
|
23
|
+
)
|
|
24
|
+
from benchkit.evaluation import EvaluationResult
|
|
25
|
+
|
|
26
|
+
DATASET = "HuggingFaceM4/ChartQA"
|
|
27
|
+
PARQUET = "data/test-00000-of-00001-e2cd0b7a0f9eb20d.parquet"
|
|
28
|
+
REVISION = "b605b6e08b57faf4359aeb2fe6a3ca595f99b6c5"
|
|
29
|
+
|
|
30
|
+
INSTRUCTION = (
|
|
31
|
+
"Answer the question about the chart. You may reason first, but end with "
|
|
32
|
+
'a final line of the form "Answer: X", where X is a single word or a '
|
|
33
|
+
"number without units."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_ANSWER = re.compile(r"answer\s*[::]\s*(.+)", re.IGNORECASE)
|
|
37
|
+
_WRAPPERS = " \t`'\"*_$"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _convert(index: int, row: dict) -> dict:
|
|
41
|
+
return {
|
|
42
|
+
"task_id": f"chartqa/{index}",
|
|
43
|
+
"category": "augmented" if row["human_or_machine"] else "human",
|
|
44
|
+
"question": row["query"],
|
|
45
|
+
"answers": list(row["label"]),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def extract_answer(response: str) -> str:
|
|
50
|
+
"""The text after the last "Answer:", else the last non-empty line."""
|
|
51
|
+
text = strip_think_tags(response).strip()
|
|
52
|
+
found = _ANSWER.findall(text)
|
|
53
|
+
if found:
|
|
54
|
+
answer = found[-1]
|
|
55
|
+
else:
|
|
56
|
+
lines = [line for line in text.splitlines() if line.strip()]
|
|
57
|
+
answer = lines[-1] if lines else ""
|
|
58
|
+
return answer.strip(_WRAPPERS).removesuffix(".").strip(_WRAPPERS)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _to_floats(text: str) -> list[float]:
|
|
62
|
+
"""Numeric readings of an answer: a percent is tried over 100 and raw."""
|
|
63
|
+
text = text.replace(",", "").strip()
|
|
64
|
+
try:
|
|
65
|
+
value = float(text.removesuffix("%").strip())
|
|
66
|
+
except ValueError:
|
|
67
|
+
return []
|
|
68
|
+
return [value / 100.0, value] if text.endswith("%") else [value]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def relaxed_match(prediction: str, target: str, tolerance: float = 0.05) -> bool:
|
|
72
|
+
expected = _to_floats(target)
|
|
73
|
+
predicted = _to_floats(prediction)
|
|
74
|
+
if predicted and expected and expected[0]:
|
|
75
|
+
return any(
|
|
76
|
+
abs(value - expected[0]) / abs(expected[0]) <= tolerance
|
|
77
|
+
for value in predicted
|
|
78
|
+
)
|
|
79
|
+
return prediction.strip().lower() == target.strip().lower()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@cache
|
|
83
|
+
def _rows() -> tuple[dict, ...]:
|
|
84
|
+
return tuple(load_rows("chartqa", DATASET, PARQUET, REVISION, _convert))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ChartQA:
|
|
88
|
+
name = "chartqa"
|
|
89
|
+
vision = True
|
|
90
|
+
task_count = 2500
|
|
91
|
+
data_source = vision_dataset(
|
|
92
|
+
"chartqa", REVISION, "ChartQA test charts from Hugging Face", _rows
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def load_tasks(self) -> list[Task]:
|
|
96
|
+
tasks = [
|
|
97
|
+
Task(
|
|
98
|
+
id=row["task_id"],
|
|
99
|
+
prompt=row["question"],
|
|
100
|
+
metadata={
|
|
101
|
+
"answers": row["answers"],
|
|
102
|
+
"category": row["category"],
|
|
103
|
+
"images": [row["image"]],
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
for row in _rows()
|
|
107
|
+
]
|
|
108
|
+
return spread(tasks, lambda task: task.metadata["category"])
|
|
109
|
+
|
|
110
|
+
def build_prompt(self, task: Task) -> str:
|
|
111
|
+
return f"{INSTRUCTION}\n\nQuestion: {task.prompt}"
|
|
112
|
+
|
|
113
|
+
def result_metadata(self, variant: str | None = None) -> dict:
|
|
114
|
+
return {"dataset_revision": REVISION}
|
|
115
|
+
|
|
116
|
+
def evaluate_with_feedback(self, task: Task, response: str) -> EvaluationResult:
|
|
117
|
+
predicted = extract_answer(response)
|
|
118
|
+
passed = any(relaxed_match(predicted, a) for a in task.metadata["answers"])
|
|
119
|
+
return EvaluationResult(
|
|
120
|
+
score=float(passed),
|
|
121
|
+
feedback=""
|
|
122
|
+
if passed
|
|
123
|
+
else 'The final answer is wrong. End with one line "Answer: X".',
|
|
124
|
+
details={"predicted_answer": predicted},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def evaluate(self, task: Task, response: str) -> bool:
|
|
128
|
+
return self.evaluate_with_feedback(task, response).passed
|
|
129
|
+
|
|
130
|
+
def summary_fields(self, records: list[object]) -> dict:
|
|
131
|
+
categories = {row["task_id"]: row["category"] for row in _rows()}
|
|
132
|
+
scores = category_scores(records, categories)
|
|
133
|
+
return {"category_scores": scores} if scores else {}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Code verifier - does a model-written HumanEval+ solution pass its tests?"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from benchkit.benchmarks.base import Task
|
|
8
|
+
from benchkit.benchmarks.utils import strip_think_tags
|
|
9
|
+
|
|
10
|
+
DATASET = Path(__file__).parent.parent / "datasets" / "code_verifier.jsonl"
|
|
11
|
+
|
|
12
|
+
# One neutral question for both harnesses, so neither is nudged either way.
|
|
13
|
+
QUESTION = "Does this solution correctly solve the problem?"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _extract_answer(response: str) -> str | None:
|
|
17
|
+
text = strip_think_tags(response)
|
|
18
|
+
answers = re.findall(r"\b(?:yes|no)\b", text.lower())
|
|
19
|
+
return answers[-1] if answers else None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CodeVerifier:
|
|
23
|
+
name = "code-verifier"
|
|
24
|
+
# Yes/no without a choices list; two options means no repair.
|
|
25
|
+
answer_options = 2
|
|
26
|
+
decision_instructions = QUESTION
|
|
27
|
+
task_count = 500
|
|
28
|
+
|
|
29
|
+
def load_tasks(self) -> list[Task]:
|
|
30
|
+
tasks = []
|
|
31
|
+
with open(DATASET, encoding="utf-8") as f:
|
|
32
|
+
for line in f:
|
|
33
|
+
d = json.loads(line)
|
|
34
|
+
tasks.append(
|
|
35
|
+
Task(
|
|
36
|
+
id=d["id"],
|
|
37
|
+
prompt=(
|
|
38
|
+
f"Problem:\n```python\n{d['prompt'].strip()}\n```\n\n"
|
|
39
|
+
f"Solution:\n```python\n"
|
|
40
|
+
f"{d['solution'].strip()}\n```"
|
|
41
|
+
),
|
|
42
|
+
metadata={
|
|
43
|
+
"answer": "yes" if d["passes"] else "no",
|
|
44
|
+
"task_id": d["task_id"],
|
|
45
|
+
"model": d["model"],
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
return tasks
|
|
50
|
+
|
|
51
|
+
def build_prompt(self, task: Task) -> str:
|
|
52
|
+
return f"{task.prompt}\n\n{QUESTION} Reply with only YES or NO."
|
|
53
|
+
|
|
54
|
+
def evaluate(self, task: Task, response: str) -> bool:
|
|
55
|
+
return _extract_answer(response) == task.metadata["answer"]
|
|
56
|
+
|
|
57
|
+
def summary_fields(self, records: list[object]) -> dict:
|
|
58
|
+
"""Score passing and failing solutions separately.
|
|
59
|
+
|
|
60
|
+
The suite is balanced, so a verifier that always says "yes" scores
|
|
61
|
+
50% overall; the per-class split shows which way a model leans.
|
|
62
|
+
"""
|
|
63
|
+
answers = {task.id: task.metadata["answer"] for task in self.load_tasks()}
|
|
64
|
+
passing = [r for r in records if answers.get(r.task_id) == "yes"]
|
|
65
|
+
failing = [r for r in records if answers.get(r.task_id) == "no"]
|
|
66
|
+
|
|
67
|
+
def accuracy(rows: list[object]) -> float | None:
|
|
68
|
+
if not rows:
|
|
69
|
+
return None
|
|
70
|
+
return round(sum(r.passed for r in rows) / len(rows) * 100, 1)
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"verifier_pass_total": len(passing),
|
|
74
|
+
"verifier_fail_total": len(failing),
|
|
75
|
+
"verifier_pass_accuracy": accuracy(passing),
|
|
76
|
+
"verifier_fail_accuracy": accuracy(failing),
|
|
77
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Datasets that are fetched on demand instead of shipping in the wheel.
|
|
2
|
+
|
|
3
|
+
Small suites are bundled so ``--demo`` and smoke runs work offline. Anything
|
|
4
|
+
large lives in a cache and is described by a :class:`Dataset` on the benchmark
|
|
5
|
+
class, which lets ``benchkit datasets`` and the TUI list what is installed and
|
|
6
|
+
install what is missing without loading any tasks.
|
|
7
|
+
|
|
8
|
+
Plain files such as ``mmlu.jsonl`` are :class:`RemoteFile` downloads: a pinned
|
|
9
|
+
URL plus a sha256, verified before the file is moved into place, so a partial
|
|
10
|
+
or tampered download never reaches a run. They are cached under
|
|
11
|
+
``~/.cache/benchkit/datasets`` (override with ``BENCHKIT_DATASET_CACHE``).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import os
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import httpx
|
|
25
|
+
|
|
26
|
+
Progress = Callable[[int, int | None], None]
|
|
27
|
+
"""Called with ``(bytes_done, bytes_total)`` while a download streams."""
|
|
28
|
+
|
|
29
|
+
_ATTEMPTS = 4
|
|
30
|
+
_LOCK = threading.Lock()
|
|
31
|
+
# Pinned commit of this repository that still carried the bundled copies. A
|
|
32
|
+
# commit pin plus a checksum keeps every install byte-identical to the file
|
|
33
|
+
# earlier results were scored against.
|
|
34
|
+
_PIN = "5789a5e2dcbb056abcbf32f810a1ed01de1cdad6"
|
|
35
|
+
_RAW = f"https://raw.githubusercontent.com/DogukanUrker/BenchKit/{_PIN}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def cache_root() -> Path:
|
|
39
|
+
configured = os.environ.get("BENCHKIT_DATASET_CACHE")
|
|
40
|
+
if configured:
|
|
41
|
+
return Path(configured).expanduser()
|
|
42
|
+
return Path.home() / ".cache" / "benchkit" / "datasets"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def fmt_bytes(size: float) -> str:
|
|
46
|
+
"""Decimal units, matching rich's download progress column."""
|
|
47
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
48
|
+
if size < 1000 or unit == "GB":
|
|
49
|
+
return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}"
|
|
50
|
+
size /= 1000
|
|
51
|
+
raise AssertionError("unreachable")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class Dataset:
|
|
56
|
+
"""What a benchmark needs on disk, and how to get it.
|
|
57
|
+
|
|
58
|
+
``install`` is ``None`` for data BenchKit cannot fetch itself; ``hint``
|
|
59
|
+
then says how to provide it. ``size`` is the download size in bytes when it
|
|
60
|
+
is known (``approximate`` when it can drift, like a git clone); once the
|
|
61
|
+
data is installed, listings measure what is actually on disk instead.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
summary: str
|
|
65
|
+
size: int | None
|
|
66
|
+
location: Callable[[], Path]
|
|
67
|
+
is_installed: Callable[[], bool]
|
|
68
|
+
install: Callable[[Progress | None], object] | None
|
|
69
|
+
hint: str = ""
|
|
70
|
+
approximate: bool = False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class RemoteFile:
|
|
75
|
+
"""One checksummed file fetched into the dataset cache on first use."""
|
|
76
|
+
|
|
77
|
+
name: str
|
|
78
|
+
url: str
|
|
79
|
+
sha256: str
|
|
80
|
+
size: int
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def path(self) -> Path:
|
|
84
|
+
return cache_root() / self.name
|
|
85
|
+
|
|
86
|
+
def installed(self) -> bool:
|
|
87
|
+
path = self.path
|
|
88
|
+
return path.is_file() and path.stat().st_size == self.size
|
|
89
|
+
|
|
90
|
+
def ensure(self, progress: Progress | None = None) -> Path:
|
|
91
|
+
"""Return the cached path, downloading and verifying it if needed."""
|
|
92
|
+
with _LOCK:
|
|
93
|
+
if not self.installed():
|
|
94
|
+
self._download(progress)
|
|
95
|
+
return self.path
|
|
96
|
+
|
|
97
|
+
def source(self, summary: str) -> Dataset:
|
|
98
|
+
return Dataset(
|
|
99
|
+
summary=summary,
|
|
100
|
+
size=self.size,
|
|
101
|
+
location=lambda: self.path,
|
|
102
|
+
is_installed=self.installed,
|
|
103
|
+
install=self.ensure,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _download(self, progress: Progress | None) -> None:
|
|
107
|
+
target = self.path
|
|
108
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
partial = target.with_name(target.name + ".part")
|
|
110
|
+
for attempt in range(_ATTEMPTS):
|
|
111
|
+
try:
|
|
112
|
+
digest = hashlib.sha256()
|
|
113
|
+
done = 0
|
|
114
|
+
with httpx.stream(
|
|
115
|
+
"GET", self.url, follow_redirects=True, timeout=60
|
|
116
|
+
) as r:
|
|
117
|
+
r.raise_for_status()
|
|
118
|
+
with open(partial, "wb") as f:
|
|
119
|
+
for chunk in r.iter_bytes():
|
|
120
|
+
f.write(chunk)
|
|
121
|
+
digest.update(chunk)
|
|
122
|
+
done += len(chunk)
|
|
123
|
+
if progress:
|
|
124
|
+
progress(done, self.size)
|
|
125
|
+
break
|
|
126
|
+
except (httpx.TransportError, httpx.HTTPStatusError) as exc:
|
|
127
|
+
retryable = not isinstance(exc, httpx.HTTPStatusError) or (
|
|
128
|
+
exc.response.status_code in {429, 500, 502, 503, 504}
|
|
129
|
+
)
|
|
130
|
+
if not retryable or attempt == _ATTEMPTS - 1:
|
|
131
|
+
partial.unlink(missing_ok=True)
|
|
132
|
+
raise DatasetUnavailable(self, str(exc)) from exc
|
|
133
|
+
time.sleep(2**attempt)
|
|
134
|
+
|
|
135
|
+
if digest.hexdigest() != self.sha256:
|
|
136
|
+
partial.unlink(missing_ok=True)
|
|
137
|
+
raise DatasetUnavailable(
|
|
138
|
+
self, f"checksum mismatch (got {digest.hexdigest()[:12]}…)"
|
|
139
|
+
)
|
|
140
|
+
partial.replace(target)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class DatasetUnavailable(RuntimeError):
|
|
144
|
+
"""A dataset is not cached and could not be fetched."""
|
|
145
|
+
|
|
146
|
+
def __init__(self, remote: RemoteFile, reason: str) -> None:
|
|
147
|
+
super().__init__(
|
|
148
|
+
f"{remote.name} is not installed and could not be downloaded "
|
|
149
|
+
f"({reason}). Run `benchkit datasets install` once you are online, "
|
|
150
|
+
f"or place the file at {remote.path} (sha256 {remote.sha256})."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
MMLU = RemoteFile(
|
|
155
|
+
name="mmlu.jsonl",
|
|
156
|
+
url=f"{_RAW}/src/benchkit/datasets/mmlu.jsonl",
|
|
157
|
+
sha256="e3e9beb9b3f6b342b99ef59ead76a03c7553fb86705ce17ce95e58e51c1cc37d",
|
|
158
|
+
size=8_563_175,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
MMLU_PRO = RemoteFile(
|
|
162
|
+
name="mmlu_pro.jsonl",
|
|
163
|
+
url=f"{_RAW}/src/benchkit/datasets/mmlu_pro.jsonl",
|
|
164
|
+
sha256="92ec532d9e2bae342ec7256005c20b6e5defbed76504280df775fb77068f077e",
|
|
165
|
+
size=10_260_787,
|
|
166
|
+
)
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""EvalPlus benchmarks with stronger HumanEval+ and MBPP+ test suites."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import multiprocessing
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import textwrap
|
|
10
|
+
from functools import cache, lru_cache
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from benchkit.benchmarks.base import Task
|
|
15
|
+
from benchkit.benchmarks.downloads import Dataset
|
|
16
|
+
from benchkit.benchmarks.utils import strip_think_tags
|
|
17
|
+
|
|
18
|
+
HUMANEVAL_SYSTEM = (
|
|
19
|
+
"Complete the following Python function. "
|
|
20
|
+
"Output ONLY the function body. "
|
|
21
|
+
"No markdown fences, no explanation, no examples."
|
|
22
|
+
)
|
|
23
|
+
MBPP_SYSTEM = (
|
|
24
|
+
"Write a Python function to solve the following task. "
|
|
25
|
+
"Output ONLY the function. "
|
|
26
|
+
"No markdown fences, no explanation."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@contextlib.contextmanager
|
|
31
|
+
def _fork_multiprocessing():
|
|
32
|
+
"""Temporarily swap ``multiprocessing`` primitives to use the *fork* context.
|
|
33
|
+
|
|
34
|
+
EvalPlus calls ``multiprocessing.Process`` (and friends) using the
|
|
35
|
+
module-level default, which on Python 3.13 / macOS is ``spawn``. The
|
|
36
|
+
``spawn`` path validates every FD it intends to keep open across
|
|
37
|
+
``fork_exec``, raising ``ValueError: bad value(s) in fds_to_keep`` when
|
|
38
|
+
the Textual TUI has high-numbered or already-closed descriptors.
|
|
39
|
+
|
|
40
|
+
Switching to ``fork`` side-steps the issue entirely: the child inherits a
|
|
41
|
+
copy of the parent's address space and no FDs need to be explicitly
|
|
42
|
+
forwarded. The swap is scoped so the rest of the process is unaffected.
|
|
43
|
+
|
|
44
|
+
EvalPlus's ``evalplus.eval`` does ``from multiprocessing import Array, Value``
|
|
45
|
+
so those names are bound on that module and must be patched there as well.
|
|
46
|
+
"""
|
|
47
|
+
import evalplus.eval as _ev
|
|
48
|
+
|
|
49
|
+
fork_ctx = multiprocessing.get_context("fork")
|
|
50
|
+
mp_originals = (
|
|
51
|
+
multiprocessing.Process,
|
|
52
|
+
multiprocessing.Value,
|
|
53
|
+
multiprocessing.Array,
|
|
54
|
+
)
|
|
55
|
+
ev_originals = (_ev.Value, _ev.Array)
|
|
56
|
+
try:
|
|
57
|
+
multiprocessing.Process = fork_ctx.Process
|
|
58
|
+
multiprocessing.Value = fork_ctx.Value
|
|
59
|
+
multiprocessing.Array = fork_ctx.Array
|
|
60
|
+
_ev.Value = fork_ctx.Value
|
|
61
|
+
_ev.Array = fork_ctx.Array
|
|
62
|
+
yield
|
|
63
|
+
finally:
|
|
64
|
+
multiprocessing.Process, multiprocessing.Value, multiprocessing.Array = (
|
|
65
|
+
mp_originals
|
|
66
|
+
)
|
|
67
|
+
_ev.Value, _ev.Array = ev_originals
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _evalplus_api():
|
|
71
|
+
"""Import EvalPlus lazily so normal startup stays lightweight."""
|
|
72
|
+
# RLIMIT_AS is not reliably supported on macOS. EvalPlus otherwise tries
|
|
73
|
+
# to impose a 4 GiB address-space limit that can make every check fail
|
|
74
|
+
# before generated code starts.
|
|
75
|
+
if platform.system() == "Darwin":
|
|
76
|
+
os.environ.setdefault("EVALPLUS_MAX_MEMORY_BYTES", "-1")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
from evalplus.data import get_human_eval_plus, get_mbpp_plus
|
|
80
|
+
from evalplus.eval import PASS
|
|
81
|
+
from evalplus.eval._special_oracle import MBPP_OUTPUT_NOT_NONE_TASKS
|
|
82
|
+
from evalplus.evaluate import check_correctness
|
|
83
|
+
from evalplus.gen.util import trusted_exec
|
|
84
|
+
except ImportError as exc:
|
|
85
|
+
raise RuntimeError(
|
|
86
|
+
"EvalPlus is missing from the environment. Run: uv sync"
|
|
87
|
+
) from exc
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
get_human_eval_plus,
|
|
91
|
+
get_mbpp_plus,
|
|
92
|
+
PASS,
|
|
93
|
+
MBPP_OUTPUT_NOT_NONE_TASKS,
|
|
94
|
+
check_correctness,
|
|
95
|
+
trusted_exec,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@lru_cache(maxsize=2)
|
|
100
|
+
def _problems(dataset: str) -> dict[str, dict[str, Any]]:
|
|
101
|
+
get_human_eval_plus, get_mbpp_plus, *_ = _evalplus_api()
|
|
102
|
+
if dataset == "humaneval":
|
|
103
|
+
return get_human_eval_plus()
|
|
104
|
+
if dataset == "mbpp":
|
|
105
|
+
return get_mbpp_plus()
|
|
106
|
+
raise ValueError(f"Unsupported EvalPlus dataset: {dataset}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@cache
|
|
110
|
+
def _expected_output(dataset: str, task_id: str) -> dict[str, list[Any]]:
|
|
111
|
+
"""Compute the official oracle outputs once per task and process."""
|
|
112
|
+
(
|
|
113
|
+
_,
|
|
114
|
+
_,
|
|
115
|
+
_,
|
|
116
|
+
output_not_none_tasks,
|
|
117
|
+
_,
|
|
118
|
+
trusted_exec,
|
|
119
|
+
) = _evalplus_api()
|
|
120
|
+
problem = _problems(dataset)[task_id]
|
|
121
|
+
oracle_code = problem["prompt"] + problem["canonical_solution"]
|
|
122
|
+
output_not_none = (
|
|
123
|
+
dataset == "mbpp" and problem["entry_point"] in output_not_none_tasks
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
base, base_time = trusted_exec(
|
|
127
|
+
oracle_code,
|
|
128
|
+
problem["base_input"],
|
|
129
|
+
problem["entry_point"],
|
|
130
|
+
record_time=True,
|
|
131
|
+
output_not_none=output_not_none,
|
|
132
|
+
)
|
|
133
|
+
plus, plus_time = trusted_exec(
|
|
134
|
+
oracle_code,
|
|
135
|
+
problem["plus_input"],
|
|
136
|
+
problem["entry_point"],
|
|
137
|
+
record_time=True,
|
|
138
|
+
output_not_none=output_not_none,
|
|
139
|
+
)
|
|
140
|
+
return {
|
|
141
|
+
"base": base,
|
|
142
|
+
"base_time": base_time,
|
|
143
|
+
"plus": plus,
|
|
144
|
+
"plus_time": plus_time,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _extract_code(response: str) -> str:
|
|
149
|
+
"""Strip reasoning and a single Markdown code fence."""
|
|
150
|
+
text = strip_think_tags(response).rstrip()
|
|
151
|
+
if "```python" in text:
|
|
152
|
+
text = text.split("```python", 1)[1].split("```", 1)[0]
|
|
153
|
+
elif "```" in text:
|
|
154
|
+
text = text.split("```", 1)[1].split("```", 1)[0]
|
|
155
|
+
return text.strip("\n")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _human_solution(problem: dict[str, Any], response: str) -> str:
|
|
159
|
+
code = _extract_code(response)
|
|
160
|
+
entry_point = problem["entry_point"]
|
|
161
|
+
if f"def {entry_point}" not in code:
|
|
162
|
+
normalized = textwrap.dedent(code)
|
|
163
|
+
solution = problem["prompt"] + textwrap.indent(normalized, " ")
|
|
164
|
+
try:
|
|
165
|
+
compile(solution, "<benchkit-humaneval>", "exec")
|
|
166
|
+
except (IndentationError, TabError):
|
|
167
|
+
lines = code.splitlines()
|
|
168
|
+
first_index = next(
|
|
169
|
+
(index for index, line in enumerate(lines) if line.strip()), None
|
|
170
|
+
)
|
|
171
|
+
if first_index is not None and not lines[first_index].startswith(
|
|
172
|
+
(" ", "\t")
|
|
173
|
+
):
|
|
174
|
+
lines[first_index] = " " + lines[first_index]
|
|
175
|
+
normalized = textwrap.dedent("\n".join(lines))
|
|
176
|
+
repaired = problem["prompt"] + textwrap.indent(normalized, " ")
|
|
177
|
+
try:
|
|
178
|
+
compile(repaired, "<benchkit-humaneval>", "exec")
|
|
179
|
+
except (IndentationError, TabError):
|
|
180
|
+
pass
|
|
181
|
+
else:
|
|
182
|
+
solution = repaired
|
|
183
|
+
return solution
|
|
184
|
+
|
|
185
|
+
imports = [
|
|
186
|
+
line
|
|
187
|
+
for line in problem["prompt"].splitlines()
|
|
188
|
+
if line.startswith(("import ", "from "))
|
|
189
|
+
]
|
|
190
|
+
import_block = "\n".join(imports)
|
|
191
|
+
return f"{import_block}\n\n{code}" if imports else code
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _evalplus_cache(dataset: str) -> Path:
|
|
195
|
+
from evalplus.data.humaneval import HUMANEVAL_PLUS_VERSION
|
|
196
|
+
from evalplus.data.mbpp import MBPP_PLUS_VERSION
|
|
197
|
+
from evalplus.data.utils import get_dataset_metadata
|
|
198
|
+
|
|
199
|
+
if dataset == "humaneval":
|
|
200
|
+
name, version = "HumanEvalPlus", HUMANEVAL_PLUS_VERSION
|
|
201
|
+
else:
|
|
202
|
+
name, version = "MbppPlus", MBPP_PLUS_VERSION
|
|
203
|
+
return Path(get_dataset_metadata(name, version, mini=False)[1])
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _evalplus_installed(dataset: str) -> bool:
|
|
207
|
+
try:
|
|
208
|
+
return _evalplus_cache(dataset).exists()
|
|
209
|
+
except ImportError:
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _evalplus_source(dataset: str, summary: str, size: int) -> Dataset:
|
|
214
|
+
return Dataset(
|
|
215
|
+
summary=summary,
|
|
216
|
+
size=size,
|
|
217
|
+
location=lambda: _evalplus_cache(dataset),
|
|
218
|
+
is_installed=lambda: _evalplus_installed(dataset),
|
|
219
|
+
install=lambda _progress: _quiet_problems(dataset),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _quiet_problems(dataset: str) -> None:
|
|
224
|
+
# EvalPlus downloads through wget, which draws its own progress bar on
|
|
225
|
+
# stdout and tears up the CLI's live progress line and the TUI.
|
|
226
|
+
with open(os.devnull, "w") as sink, contextlib.redirect_stdout(sink):
|
|
227
|
+
_problems(dataset)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class _EvalPlusBenchmark:
|
|
231
|
+
dataset: str
|
|
232
|
+
name: str
|
|
233
|
+
task_count: int
|
|
234
|
+
system: str
|
|
235
|
+
evaluation_activity = "executing code and running EvalPlus tests"
|
|
236
|
+
|
|
237
|
+
def load_tasks(self) -> list[Task]:
|
|
238
|
+
return [
|
|
239
|
+
Task(
|
|
240
|
+
id=task_id,
|
|
241
|
+
prompt=problem["prompt"],
|
|
242
|
+
metadata={
|
|
243
|
+
"entry_point": problem["entry_point"],
|
|
244
|
+
"canonical_solution": problem["canonical_solution"],
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
for task_id, problem in _problems(self.dataset).items()
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
def build_prompt(self, task: Task) -> str:
|
|
251
|
+
return f"{self.system}\n\n{task.prompt}"
|
|
252
|
+
|
|
253
|
+
def evaluate(self, task: Task, response: str) -> bool:
|
|
254
|
+
*_, pass_status, _, check_correctness, _ = _evalplus_api()
|
|
255
|
+
problem = _problems(self.dataset)[task.id]
|
|
256
|
+
if self.dataset == "humaneval":
|
|
257
|
+
solution = _human_solution(problem, response)
|
|
258
|
+
else:
|
|
259
|
+
solution = _extract_code(response)
|
|
260
|
+
|
|
261
|
+
with _fork_multiprocessing():
|
|
262
|
+
result = check_correctness(
|
|
263
|
+
self.dataset,
|
|
264
|
+
completion_id=0,
|
|
265
|
+
problem=problem,
|
|
266
|
+
solution=solution,
|
|
267
|
+
expected_output=_expected_output(self.dataset, task.id),
|
|
268
|
+
fast_check=True,
|
|
269
|
+
)
|
|
270
|
+
return result["base"][0] == pass_status and result["plus"][0] == pass_status
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class HumanEvalPlus(_EvalPlusBenchmark):
|
|
274
|
+
name = "humaneval-plus"
|
|
275
|
+
dataset = "humaneval"
|
|
276
|
+
task_count = 164
|
|
277
|
+
system = HUMANEVAL_SYSTEM
|
|
278
|
+
data_source = _evalplus_source(
|
|
279
|
+
"humaneval", "HumanEval+ release from EvalPlus", size=925_932
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class MBPPPlus(_EvalPlusBenchmark):
|
|
284
|
+
name = "mbpp-plus"
|
|
285
|
+
dataset = "mbpp"
|
|
286
|
+
task_count = 378
|
|
287
|
+
system = MBPP_SYSTEM
|
|
288
|
+
data_source = _evalplus_source("mbpp", "MBPP+ release from EvalPlus", size=336_032)
|