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,535 @@
|
|
|
1
|
+
"""Deterministic synthetic task generation for the RULER benchmark."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import random
|
|
8
|
+
import string
|
|
9
|
+
import urllib.request
|
|
10
|
+
import uuid
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from benchkit.benchmarks.base import Task
|
|
15
|
+
from benchkit.leakage import assert_candidate_parity
|
|
16
|
+
|
|
17
|
+
# Providers use both binary and decimal interpretations of "128K". The final
|
|
18
|
+
# target is 128,000 so a server advertising a 128,000-token served window can
|
|
19
|
+
# run the bucket instead of being rejected for falling 3,072 tokens short.
|
|
20
|
+
CONTEXT_BUCKETS = (4096, 8192, 16384, 32768, 65536, 128000)
|
|
21
|
+
DEFAULT_SAMPLES_PER_KIND = 500
|
|
22
|
+
PRACTICAL_SAMPLES_PER_KIND = 3
|
|
23
|
+
TASK_KINDS = (
|
|
24
|
+
"niah_single_1",
|
|
25
|
+
"niah_single_2",
|
|
26
|
+
"niah_single_3",
|
|
27
|
+
"niah_multikey_1",
|
|
28
|
+
"niah_multikey_2",
|
|
29
|
+
"niah_multikey_3",
|
|
30
|
+
"niah_multivalue",
|
|
31
|
+
"niah_multiquery",
|
|
32
|
+
"variable_tracking",
|
|
33
|
+
"cwe",
|
|
34
|
+
"fwe",
|
|
35
|
+
"qa_1",
|
|
36
|
+
"qa_2",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
TASKS_PER_BUCKET = DEFAULT_SAMPLES_PER_KIND * len(TASK_KINDS)
|
|
41
|
+
|
|
42
|
+
# RULER lengths include the expected completion. Keep enough room for a short
|
|
43
|
+
# answer and the server's chat template, neither of which is visible to the
|
|
44
|
+
# benchmark-side tokenizer call.
|
|
45
|
+
OUTPUT_RESERVE_TOKENS = 256
|
|
46
|
+
FALLBACK_CHARS_PER_TOKEN = 4
|
|
47
|
+
FIT_TOLERANCE = 0.01
|
|
48
|
+
|
|
49
|
+
_VARIABLE_NOISE = (
|
|
50
|
+
"The grass is green. The sky is blue. The sun is yellow. "
|
|
51
|
+
"Here we go. There and back again."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def context_label(tokens: int) -> str:
|
|
56
|
+
"""Return the compact label used for a context bucket."""
|
|
57
|
+
if tokens == 128000:
|
|
58
|
+
return "128k"
|
|
59
|
+
if tokens % 1024 == 0:
|
|
60
|
+
return f"{tokens // 1024}k"
|
|
61
|
+
if tokens % 1000 == 0:
|
|
62
|
+
return f"{tokens // 1000}k"
|
|
63
|
+
return f"{tokens:,}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _seed(bucket: int, sample: int, kind: str) -> int:
|
|
67
|
+
kind_offset = (
|
|
68
|
+
53
|
|
69
|
+
if kind == "variable_tracking"
|
|
70
|
+
else sum((index + 1) * ord(char) for index, char in enumerate(kind))
|
|
71
|
+
)
|
|
72
|
+
return bucket * 1009 + sample * 97 + kind_offset
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _unique_variables(rng: random.Random, count: int) -> list[str]:
|
|
76
|
+
values: list[str] = []
|
|
77
|
+
while len(values) < count:
|
|
78
|
+
value = "".join(rng.choices(string.ascii_uppercase, k=6))
|
|
79
|
+
if value not in values:
|
|
80
|
+
values.append(value)
|
|
81
|
+
return values
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _random_uuid(rng: random.Random) -> str:
|
|
85
|
+
return str(uuid.UUID(int=rng.getrandbits(128), version=4))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _random_word(rng: random.Random) -> str:
|
|
89
|
+
return f"{''.join(rng.choices(string.ascii_lowercase, k=7))}-{''.join(rng.choices(string.ascii_lowercase, k=8))}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _niah_task(bucket: int, sample: int, kind: str) -> Task:
|
|
93
|
+
seed = _seed(bucket, sample, kind)
|
|
94
|
+
rng = random.Random(seed)
|
|
95
|
+
config = {
|
|
96
|
+
"niah_single_1": ("noise", "word", "number", 1, 1, 1),
|
|
97
|
+
"niah_single_2": ("essay", "word", "number", 1, 1, 1),
|
|
98
|
+
"niah_single_3": ("essay", "word", "uuid", 1, 1, 1),
|
|
99
|
+
"niah_multikey_1": ("essay", "word", "number", 4, 1, 1),
|
|
100
|
+
"niah_multikey_2": ("needle", "word", "number", 1, 1, 1),
|
|
101
|
+
"niah_multikey_3": ("needle", "uuid", "uuid", 1, 1, 1),
|
|
102
|
+
"niah_multivalue": ("essay", "word", "number", 1, 4, 1),
|
|
103
|
+
"niah_multiquery": ("essay", "word", "number", 4, 1, 4),
|
|
104
|
+
}[kind]
|
|
105
|
+
haystack, key_type, value_type, key_count, value_count, query_count = config
|
|
106
|
+
|
|
107
|
+
def generate(requested_type: str) -> str:
|
|
108
|
+
if requested_type == "number":
|
|
109
|
+
return str(rng.randint(1_000_000, 9_999_999))
|
|
110
|
+
if requested_type == "uuid":
|
|
111
|
+
return _random_uuid(rng)
|
|
112
|
+
return _random_word(rng)
|
|
113
|
+
|
|
114
|
+
keys = [generate(key_type) for _ in range(key_count)]
|
|
115
|
+
values = [[generate(value_type) for _ in range(value_count)] for _ in keys]
|
|
116
|
+
query_indices = rng.sample(range(key_count), query_count)
|
|
117
|
+
probe_rng = random.Random(seed ^ 0xD157AC7)
|
|
118
|
+
probe_pairs = [_distractor_pair(probe_rng, key_type, value_type) for _ in range(16)]
|
|
119
|
+
assert_candidate_parity(
|
|
120
|
+
target_keys=keys,
|
|
121
|
+
distractor_keys=[key for key, _ in probe_pairs],
|
|
122
|
+
target_values=[value for group in values for value in group],
|
|
123
|
+
distractor_values=[value for _, value in probe_pairs],
|
|
124
|
+
)
|
|
125
|
+
label = context_label(bucket)
|
|
126
|
+
return Task(
|
|
127
|
+
id=f"RULER/{label}/{kind}/{sample:03d}",
|
|
128
|
+
prompt="",
|
|
129
|
+
metadata={
|
|
130
|
+
"variant": label,
|
|
131
|
+
"context_tokens": bucket,
|
|
132
|
+
"task_type": kind,
|
|
133
|
+
"seed": seed,
|
|
134
|
+
"haystack": haystack,
|
|
135
|
+
"key_type": key_type,
|
|
136
|
+
"value_type": value_type,
|
|
137
|
+
"keys": keys,
|
|
138
|
+
"values": values,
|
|
139
|
+
"query": [keys[index] for index in query_indices],
|
|
140
|
+
"answers": [value for index in query_indices for value in values[index]],
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _variable_task(bucket: int, sample: int) -> Task:
|
|
146
|
+
seed = _seed(bucket, sample, "variable_tracking")
|
|
147
|
+
rng = random.Random(seed)
|
|
148
|
+
variables = _unique_variables(rng, 5)
|
|
149
|
+
value = str(rng.randint(10_000, 99_999))
|
|
150
|
+
label = context_label(bucket)
|
|
151
|
+
return Task(
|
|
152
|
+
id=f"RULER/{label}/variable_tracking/{sample:02d}",
|
|
153
|
+
prompt="",
|
|
154
|
+
metadata={
|
|
155
|
+
"variant": label,
|
|
156
|
+
"context_tokens": bucket,
|
|
157
|
+
"task_type": "variable_tracking",
|
|
158
|
+
"seed": seed,
|
|
159
|
+
"variables": variables,
|
|
160
|
+
"query": value,
|
|
161
|
+
"answers": variables,
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def generate_tasks(*, samples_per_kind: int = DEFAULT_SAMPLES_PER_KIND) -> list[Task]:
|
|
167
|
+
"""Create lightweight task specifications for every context bucket."""
|
|
168
|
+
if samples_per_kind < 1:
|
|
169
|
+
raise ValueError("RULER samples per task must be positive")
|
|
170
|
+
tasks: list[Task] = []
|
|
171
|
+
for bucket in CONTEXT_BUCKETS:
|
|
172
|
+
# Keep each sample's 13 task families together so small CLI slices are
|
|
173
|
+
# representative instead of containing only the first NIAH family.
|
|
174
|
+
for sample in range(samples_per_kind):
|
|
175
|
+
for kind in TASK_KINDS:
|
|
176
|
+
tasks.append(
|
|
177
|
+
_variable_task(bucket, sample)
|
|
178
|
+
if kind == "variable_tracking"
|
|
179
|
+
else _niah_task(bucket, sample, kind)
|
|
180
|
+
if kind.startswith("niah_")
|
|
181
|
+
else _aggregation_task(bucket, sample, kind)
|
|
182
|
+
)
|
|
183
|
+
return tasks
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _aggregation_task(bucket: int, sample: int, kind: str) -> Task:
|
|
187
|
+
seed = _seed(bucket, sample, kind)
|
|
188
|
+
answers: list[str] = []
|
|
189
|
+
metadata = {
|
|
190
|
+
"variant": context_label(bucket),
|
|
191
|
+
"context_tokens": bucket,
|
|
192
|
+
"task_type": kind,
|
|
193
|
+
"seed": seed,
|
|
194
|
+
"answers": answers,
|
|
195
|
+
}
|
|
196
|
+
if kind in {"cwe", "fwe"}:
|
|
197
|
+
rng = random.Random(seed)
|
|
198
|
+
vocabulary = [
|
|
199
|
+
"".join(rng.choices(string.ascii_lowercase, k=6)) for _ in range(2000)
|
|
200
|
+
]
|
|
201
|
+
metadata["vocabulary"] = list(dict.fromkeys(vocabulary))
|
|
202
|
+
metadata["answers"] = metadata["vocabulary"][: 10 if kind == "cwe" else 3]
|
|
203
|
+
elif kind in {"qa_1", "qa_2"}:
|
|
204
|
+
metadata["qa_index"] = sample
|
|
205
|
+
return Task(
|
|
206
|
+
id=f"RULER/{context_label(bucket)}/{kind}/{sample:03d}",
|
|
207
|
+
prompt="",
|
|
208
|
+
metadata=metadata,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
_QA_URLS = {
|
|
213
|
+
"qa_1": "https://rajpurkar.github.io/SQuAD-explorer/dataset/dev-v2.0.json",
|
|
214
|
+
"qa_2": "https://huggingface.co/datasets/namlh2004/hotpotqa/resolve/7e54db4656209750ff487f6fdf8e39a66dba136b/hotpot_dev_distractor_v1.json",
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def cache_dir() -> Path:
|
|
219
|
+
return Path.home() / ".cache" / "benchkit" / "ruler"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
ESSAY_FILE = "PaulGrahamEssays.json"
|
|
223
|
+
QA_FILES = {"qa_1": "squad.json", "qa_2": "hotpotqa.json"}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def corpus_installed() -> bool:
|
|
227
|
+
root = cache_dir()
|
|
228
|
+
return all((root / name).exists() for name in (ESSAY_FILE, *QA_FILES.values()))
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def install_corpus() -> None:
|
|
232
|
+
"""Fetch the QA corpora; the essay corpus has no pinned download."""
|
|
233
|
+
for kind in QA_FILES:
|
|
234
|
+
_qa_path(kind)
|
|
235
|
+
if not (cache_dir() / ESSAY_FILE).exists():
|
|
236
|
+
raise RuntimeError(
|
|
237
|
+
f"SQuAD and HotpotQA are installed, but {ESSAY_FILE} must be "
|
|
238
|
+
f"generated into {cache_dir()} with NVIDIA/RULER's "
|
|
239
|
+
"download_paulgraham_essay.py"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _qa_path(kind: str) -> Path:
|
|
244
|
+
root = cache_dir()
|
|
245
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
246
|
+
path = root / QA_FILES[kind]
|
|
247
|
+
if not path.exists():
|
|
248
|
+
try:
|
|
249
|
+
urllib.request.urlretrieve(_QA_URLS[kind], path)
|
|
250
|
+
except OSError as exc:
|
|
251
|
+
raise RuntimeError(
|
|
252
|
+
f"RULER {kind} requires {path}; download failed: {exc}"
|
|
253
|
+
) from exc
|
|
254
|
+
return path
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _essay_words() -> list[str]:
|
|
258
|
+
root = cache_dir()
|
|
259
|
+
path = root / ESSAY_FILE
|
|
260
|
+
if not path.exists():
|
|
261
|
+
raise RuntimeError(
|
|
262
|
+
f"essay-backed RULER tasks require {ESSAY_FILE} in "
|
|
263
|
+
f"{root}; generate it with NVIDIA/RULER's download_paulgraham_essay.py"
|
|
264
|
+
)
|
|
265
|
+
text = str(json.loads(path.read_text(encoding="utf-8"))["text"])
|
|
266
|
+
return text.split()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _qa_records(kind: str) -> tuple[list[dict], list[str]]:
|
|
270
|
+
raw = json.loads(_qa_path(kind).read_text(encoding="utf-8"))
|
|
271
|
+
if kind == "qa_1":
|
|
272
|
+
docs = sorted({p["context"] for d in raw["data"] for p in d["paragraphs"]})
|
|
273
|
+
index = {doc: i for i, doc in enumerate(docs)}
|
|
274
|
+
records = [
|
|
275
|
+
{
|
|
276
|
+
"query": qa["question"],
|
|
277
|
+
"answers": [a["text"] for a in qa["answers"]],
|
|
278
|
+
"docs": [index[p["context"]]],
|
|
279
|
+
}
|
|
280
|
+
for d in raw["data"]
|
|
281
|
+
for p in d["paragraphs"]
|
|
282
|
+
for qa in p["qas"]
|
|
283
|
+
if not qa.get("is_impossible")
|
|
284
|
+
]
|
|
285
|
+
else:
|
|
286
|
+
docs = sorted(
|
|
287
|
+
{
|
|
288
|
+
f"{title}\n{''.join(text)}"
|
|
289
|
+
for row in raw
|
|
290
|
+
for title, text in row["context"]
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
index = {doc: i for i, doc in enumerate(docs)}
|
|
294
|
+
records = [
|
|
295
|
+
{
|
|
296
|
+
"query": row["question"],
|
|
297
|
+
"answers": [row["answer"]],
|
|
298
|
+
"docs": [
|
|
299
|
+
index[f"{title}\n{''.join(text)}"] for title, text in row["context"]
|
|
300
|
+
],
|
|
301
|
+
}
|
|
302
|
+
for row in raw
|
|
303
|
+
]
|
|
304
|
+
return records, docs
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _render_aggregation(task: Task, filler_units: int) -> str:
|
|
308
|
+
metadata = task.metadata
|
|
309
|
+
kind = metadata["task_type"]
|
|
310
|
+
vocab = list(metadata["vocabulary"])
|
|
311
|
+
rng = random.Random(int(metadata["seed"]))
|
|
312
|
+
if kind == "cwe":
|
|
313
|
+
common = vocab[:10]
|
|
314
|
+
uncommon = vocab[10 : 10 + max(1, filler_units)]
|
|
315
|
+
words = common * 30 + uncommon * 3
|
|
316
|
+
rng.shuffle(words)
|
|
317
|
+
context = " ".join(f"{i + 1}. {word}" for i, word in enumerate(words))
|
|
318
|
+
return f"Below is a numbered list of words. In these words, some appear more often than others. Memorize the ones that appear most often.\n{context}\nQuestion: What are the 10 most common words in the above list?"
|
|
319
|
+
counts = [
|
|
320
|
+
max(1, int(max(10, filler_units) * ((i + 1) ** -2.0)))
|
|
321
|
+
for i in range(len(vocab))
|
|
322
|
+
]
|
|
323
|
+
words = [
|
|
324
|
+
word for word, count in zip(vocab, counts, strict=True) for _ in range(count)
|
|
325
|
+
]
|
|
326
|
+
rng.shuffle(words)
|
|
327
|
+
return (
|
|
328
|
+
"Read the following coded text and track the frequency of each coded word. Find the three most frequently appeared coded words. "
|
|
329
|
+
+ " ".join(words)
|
|
330
|
+
+ "\nQuestion: Do not provide any explanation. Please ignore the dots '....'. What are the three most frequently appeared words in the above coded text?"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _render_qa(task: Task, filler_units: int) -> str:
|
|
335
|
+
kind = str(task.metadata["task_type"])
|
|
336
|
+
records, docs = _qa_records(kind)
|
|
337
|
+
record = records[int(task.metadata["qa_index"]) % len(records)]
|
|
338
|
+
task.metadata["answers"] = list(record["answers"])
|
|
339
|
+
selected = list(record["docs"])
|
|
340
|
+
rng = random.Random(int(task.metadata["seed"]))
|
|
341
|
+
available = [i for i in range(len(docs)) if i not in selected]
|
|
342
|
+
selected.extend(
|
|
343
|
+
rng.sample(available, min(len(available), max(0, filler_units - len(selected))))
|
|
344
|
+
)
|
|
345
|
+
rendered = [docs[i] for i in selected]
|
|
346
|
+
rng.shuffle(rendered)
|
|
347
|
+
context = "\n\n".join(f"Document {i + 1}:\n{doc}" for i, doc in enumerate(rendered))
|
|
348
|
+
return f"Answer the question based on the given documents. Only give me the answer and do not output any other words.\n\nThe following are given documents.\n\n{context}\n\nAnswer the question based on the given documents. Only give me the answer and do not output any other words.\n\nQuestion: {record['query']}"
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _needle_line(key: str, value: str) -> str:
|
|
352
|
+
return f"One of the special magic numbers for {key} is: {value}."
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _distractor_pair(
|
|
356
|
+
rng: random.Random, key_type: str, value_type: str
|
|
357
|
+
) -> tuple[str, str]:
|
|
358
|
+
key = _random_uuid(rng) if key_type == "uuid" else _random_word(rng)
|
|
359
|
+
value = (
|
|
360
|
+
_random_uuid(rng)
|
|
361
|
+
if value_type == "uuid"
|
|
362
|
+
else str(rng.randint(1_000_000, 9_999_999))
|
|
363
|
+
)
|
|
364
|
+
return key, value
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _insert_at_positions(filler: list[str], inserts: list[str], seed: int) -> list[str]:
|
|
368
|
+
"""Distribute inserts through filler while preserving insert order."""
|
|
369
|
+
rng = random.Random(seed)
|
|
370
|
+
positions = sorted(rng.randrange(len(filler) + 1) for _ in inserts)
|
|
371
|
+
by_position: dict[int, list[str]] = {}
|
|
372
|
+
for position, value in zip(positions, inserts, strict=True):
|
|
373
|
+
by_position.setdefault(position, []).append(value)
|
|
374
|
+
|
|
375
|
+
output: list[str] = []
|
|
376
|
+
for index in range(len(filler) + 1):
|
|
377
|
+
output.extend(by_position.get(index, ()))
|
|
378
|
+
if index < len(filler):
|
|
379
|
+
output.append(filler[index])
|
|
380
|
+
return output
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _render_niah(task: Task, filler_units: int) -> str:
|
|
384
|
+
metadata = task.metadata
|
|
385
|
+
seed = int(metadata["seed"])
|
|
386
|
+
rng = random.Random(seed ^ 0xD157AC7)
|
|
387
|
+
pairs = (
|
|
388
|
+
[
|
|
389
|
+
_distractor_pair(rng, metadata["key_type"], metadata["value_type"])
|
|
390
|
+
for _ in range(filler_units)
|
|
391
|
+
]
|
|
392
|
+
if metadata["haystack"] == "needle"
|
|
393
|
+
else []
|
|
394
|
+
)
|
|
395
|
+
if pairs:
|
|
396
|
+
filler = [_needle_line(key, value) for key, value in pairs]
|
|
397
|
+
elif metadata["haystack"] == "essay":
|
|
398
|
+
words = _essay_words()
|
|
399
|
+
filler = [words[index % len(words)] for index in range(filler_units)]
|
|
400
|
+
else:
|
|
401
|
+
filler = [_VARIABLE_NOISE] * filler_units
|
|
402
|
+
needles = [
|
|
403
|
+
_needle_line(key, value)
|
|
404
|
+
for key, values in zip(metadata["keys"], metadata["values"], strict=True)
|
|
405
|
+
for value in values
|
|
406
|
+
]
|
|
407
|
+
assert_candidate_parity(
|
|
408
|
+
target_keys=list(metadata["keys"]),
|
|
409
|
+
distractor_keys=[p[0] for p in pairs],
|
|
410
|
+
target_values=list(metadata["answers"]),
|
|
411
|
+
distractor_values=[p[1] for p in pairs],
|
|
412
|
+
)
|
|
413
|
+
context = "\n".join(_insert_at_positions(filler, needles, seed ^ 0xA51CE))
|
|
414
|
+
return (
|
|
415
|
+
"Some special magic numbers are hidden within the following text. "
|
|
416
|
+
"Memorize the records because you will be asked for one key afterward.\n\n"
|
|
417
|
+
f"{context}\n\n"
|
|
418
|
+
"Question: What is the special magic number for "
|
|
419
|
+
f"{', and '.join(metadata['query'])}?\n"
|
|
420
|
+
"Answer with only the matching number."
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _render_variable_tracking(task: Task, filler_units: int) -> str:
|
|
425
|
+
metadata = task.metadata
|
|
426
|
+
variables = list(metadata["variables"])
|
|
427
|
+
query = str(metadata["query"])
|
|
428
|
+
chain = [f"VAR {variables[0]} = {query}"] + [
|
|
429
|
+
f"VAR {variables[index]} = VAR {variables[index - 1]}"
|
|
430
|
+
for index in range(1, len(variables))
|
|
431
|
+
]
|
|
432
|
+
filler = [_VARIABLE_NOISE] * filler_units
|
|
433
|
+
context = "\n".join(
|
|
434
|
+
_insert_at_positions(filler, chain, int(metadata["seed"]) ^ 0xC4A1)
|
|
435
|
+
)
|
|
436
|
+
return (
|
|
437
|
+
"Memorize and track the variable assignments hidden in the text. "
|
|
438
|
+
"Assignments are transitive: if VAR BBBBBB = VAR AAAAAA, both variables "
|
|
439
|
+
"have the same value.\n\n"
|
|
440
|
+
"Example:\n"
|
|
441
|
+
"VAR QAZWSX = 12345\n"
|
|
442
|
+
"VAR EDCVRF = VAR QAZWSX\n"
|
|
443
|
+
"For value 12345 the variables are QAZWSX, EDCVRF.\n\n"
|
|
444
|
+
f"{context}\n\n"
|
|
445
|
+
"Question: Find all variables assigned the value "
|
|
446
|
+
f"{query} in the text above.\n"
|
|
447
|
+
"Answer with only the variable names, separated by commas."
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def render_prompt(task: Task, filler_units: int) -> str:
|
|
452
|
+
"""Render a task with the requested number of synthetic filler units."""
|
|
453
|
+
kind = task.metadata["task_type"]
|
|
454
|
+
if str(kind).startswith("niah_"):
|
|
455
|
+
return _render_niah(task, max(0, filler_units))
|
|
456
|
+
if kind == "variable_tracking":
|
|
457
|
+
return _render_variable_tracking(task, max(0, filler_units))
|
|
458
|
+
if kind in {"cwe", "fwe"}:
|
|
459
|
+
return _render_aggregation(task, max(0, filler_units))
|
|
460
|
+
if kind in {"qa_1", "qa_2"}:
|
|
461
|
+
return _render_qa(task, max(1, filler_units))
|
|
462
|
+
raise ValueError(f"Unknown RULER task type: {kind}")
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _fallback_units(task: Task, target_tokens: int) -> int:
|
|
466
|
+
base = render_prompt(task, 0)
|
|
467
|
+
one = render_prompt(task, 1)
|
|
468
|
+
chars_per_unit = max(1, len(one) - len(base))
|
|
469
|
+
target_chars = target_tokens * FALLBACK_CHARS_PER_TOKEN
|
|
470
|
+
return max(0, (target_chars - len(base)) // chars_per_unit)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def fit_prompt(
|
|
474
|
+
task: Task,
|
|
475
|
+
token_count: Callable[[str], int | None] | None = None,
|
|
476
|
+
*,
|
|
477
|
+
initial_units: int | None = None,
|
|
478
|
+
) -> tuple[str, int, int | None]:
|
|
479
|
+
"""Fit a generated prompt just below its context bucket.
|
|
480
|
+
|
|
481
|
+
When the server exposes a tokenizer, a short secant-style calibration keeps
|
|
482
|
+
the prompt within one percent of the target. Generic servers fall back to a
|
|
483
|
+
conservative character-to-token estimate.
|
|
484
|
+
"""
|
|
485
|
+
bucket = int(task.metadata["context_tokens"])
|
|
486
|
+
target = max(1, bucket - OUTPUT_RESERVE_TOKENS)
|
|
487
|
+
units = (
|
|
488
|
+
max(0, initial_units)
|
|
489
|
+
if initial_units is not None
|
|
490
|
+
else _fallback_units(task, target)
|
|
491
|
+
)
|
|
492
|
+
prompt = render_prompt(task, units)
|
|
493
|
+
if token_count is None:
|
|
494
|
+
return prompt, units, None
|
|
495
|
+
|
|
496
|
+
measured = token_count(prompt)
|
|
497
|
+
if not isinstance(measured, int) or measured <= 0:
|
|
498
|
+
return prompt, units, None
|
|
499
|
+
|
|
500
|
+
base_tokens: int | None = None
|
|
501
|
+
for _ in range(4):
|
|
502
|
+
if measured <= target and measured >= target * (1 - FIT_TOLERANCE):
|
|
503
|
+
break
|
|
504
|
+
if base_tokens is None:
|
|
505
|
+
base_tokens = token_count(render_prompt(task, 0))
|
|
506
|
+
if not isinstance(base_tokens, int) or base_tokens < 0:
|
|
507
|
+
base_tokens = 0
|
|
508
|
+
|
|
509
|
+
variable_tokens = max(1, measured - base_tokens)
|
|
510
|
+
tokens_per_unit = variable_tokens / max(units, 1)
|
|
511
|
+
next_units = max(0, math.floor((target - base_tokens) / tokens_per_unit))
|
|
512
|
+
if next_units == units:
|
|
513
|
+
next_units = max(0, units - 1) if measured > target else units + 1
|
|
514
|
+
units = next_units
|
|
515
|
+
prompt = render_prompt(task, units)
|
|
516
|
+
next_measured = token_count(prompt)
|
|
517
|
+
if not isinstance(next_measured, int) or next_measured <= 0:
|
|
518
|
+
return prompt, units, None
|
|
519
|
+
measured = next_measured
|
|
520
|
+
|
|
521
|
+
# Never knowingly send a prompt above the bucket. The linear adjustment is
|
|
522
|
+
# normally exact; this final correction handles unusual tokenizer merges.
|
|
523
|
+
for _ in range(2):
|
|
524
|
+
if measured <= target or units == 0:
|
|
525
|
+
break
|
|
526
|
+
excess = measured - target
|
|
527
|
+
tokens_per_unit = max(1.0, measured / max(units, 1))
|
|
528
|
+
units = max(0, units - math.ceil(excess / tokens_per_unit) - 1)
|
|
529
|
+
prompt = render_prompt(task, units)
|
|
530
|
+
next_measured = token_count(prompt)
|
|
531
|
+
if not isinstance(next_measured, int) or next_measured <= 0:
|
|
532
|
+
return prompt, units, None
|
|
533
|
+
measured = next_measured
|
|
534
|
+
|
|
535
|
+
return prompt, units, measured
|