SeaWolf-AI commited on
Commit
26852f2
·
verified ·
1 Parent(s): dcbdb2a

Add decision core: autojev/data.py

Browse files
Files changed (1) hide show
  1. autojev/data.py +669 -0
autojev/data.py ADDED
@@ -0,0 +1,669 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build fixed, family-separated decisions from pinned public sources.
2
+
3
+ The benchmark's converters are an external, revision-pinned dependency. This
4
+ module selects records, checks separation, and records their provenance.
5
+ """
6
+
7
+ import argparse
8
+ from collections import Counter, defaultdict
9
+ from collections.abc import Iterator
10
+ from dataclasses import dataclass
11
+ import hashlib
12
+ import importlib
13
+ import json
14
+ import math
15
+ from pathlib import Path
16
+ import random
17
+ import re
18
+ import string
19
+ import sys
20
+ import tarfile
21
+ from typing import Protocol, cast
22
+ import zipfile
23
+
24
+ import httpx
25
+ from PIL import Image, ImageDraw
26
+
27
+ from autojev.events import record
28
+ from autojev.types import Content, Example, JSONValue, Label, Question, Target
29
+
30
+ NIMBLE_REVISION = "d2387fc0b32d1173bfc995395c076a25a2a107c9"
31
+ SEED = 20260920
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class Source:
36
+ repository: str
37
+ revision: str
38
+ config: str | None = None
39
+
40
+
41
+ SOURCES = {
42
+ "boolq": Source("google/boolq", "35b264d03638db9f4ce671b711558bf7ff0f80d5"),
43
+ "squad2": Source("rajpurkar/squad_v2", "3ffb306f725f7d2ce8394bc1873b24868140c412"),
44
+ "paws": Source("google-research-datasets/paws", "161ece9501cf0a11f3e48bd356eaa82de46d6a09", "labeled_final"),
45
+ "civil_comments": Source("google/civil_comments", "f2970eb3a55777454c94069077cc8d9b5866312d"),
46
+ "aegis2": Source("nvidia/Aegis-AI-Content-Safety-Dataset-2.0", "d86bb8bedff51d25ac834ab7838f1cc61acb7a2c"),
47
+ "pubmedqa": Source("qiaojin/PubMedQA", "9001f2853fb87cab8d220904e0de81ac6973b318", "pqa_labeled"),
48
+ }
49
+ ARCHIVES = {
50
+ "vitaminc": ("https://github.com/TalSchuster/talschuster.github.io/raw/master/static/vitaminc.zip", "vitaminc.zip"),
51
+ "multinli": ("https://cims.nyu.edu/~sbowman/multinli/multinli_1.0.zip", "multinli_1.0.zip"),
52
+ "massive": ("https://amazon-massive-nlu-dataset.s3.amazonaws.com/amazon-massive-dataset-1.1.tar.gz", "massive-1.1.tar.gz"),
53
+ }
54
+ ARCHIVE_SHA256 = {
55
+ "vitaminc": "49d82dc1690cbee420d18e2c26f687a7937710bb211845d2571430dfd4dc0337",
56
+ "multinli": "049f507b9e36b1fcb756cfd5aeb3b7a0cfcb84bf023793652987f7e7e0957822",
57
+ "massive": "4cba5faa11c71437928e17cb1b9b3d8b8e727e7ea363a3a9a8045e19c0491577",
58
+ }
59
+ TASKS = ("vitaminc-dev", "massive-en-US", "massive-de-DE", "boolq", "squad2",
60
+ "paws", "multinli", "civil_comments", "aegis2", "pubmedqa")
61
+ TRAIN_QUOTAS = dict(zip(TASKS, (16000, 10000, 10000, 7000, 14000, 12000, 12000, 8000, 6000, 0)))
62
+ TEST_QUOTAS = dict(zip(TASKS, (920, 540, 540, 460, 460, 385, 460, 460, 385, 385)))
63
+ DEV_QUOTAS = dict(zip(TASKS, (275, 160, 160, 140, 140, 115, 140, 140, 115, 115)))
64
+ TEMPERATURE_QUOTAS = dict(zip(TASKS, (185, 110, 110, 90, 90, 75, 90, 90, 75, 75)))
65
+ KNOWN_SPLITS = {"vitaminc": "dev", "massive": "test", "boolq": "validation",
66
+ "squad2": "validation", "paws": "test", "multinli": "dev_matched",
67
+ "civil_comments": "test", "aegis2": "test", "pubmedqa": "train"}
68
+ DEVELOPMENT_SPLITS = {**KNOWN_SPLITS, "massive": "dev", "paws": "validation",
69
+ "civil_comments": "validation", "aegis2": "validation"}
70
+ FINAL_SPLITS = {**KNOWN_SPLITS, "vitaminc": "test"}
71
+
72
+
73
+ class Converter(Protocol):
74
+ SOURCE_URL: str
75
+ LICENSE: str
76
+
77
+ def record(self, raw: dict[str, JSONValue], subset: str = "") -> dict[str, JSONValue] | None: ...
78
+
79
+
80
+ class Serializer(Protocol):
81
+ def jsonl_line(self, value: dict[str, JSONValue]) -> str: ...
82
+
83
+
84
+ class Arguments(argparse.Namespace):
85
+ cache_root: Path
86
+ source_cache: Path | None
87
+ nimble_source: Path | None
88
+ output: Path
89
+ manifest: Path
90
+ seed: int
91
+ max_length: int
92
+ train_scale: float
93
+ image_train: int
94
+ image_eval: int
95
+
96
+
97
+ def checksum(path: Path) -> str:
98
+ with path.open("rb") as stream:
99
+ return hashlib.file_digest(stream, "sha256").hexdigest()
100
+
101
+
102
+ def normalized(value: str) -> str:
103
+ return " ".join(value.casefold().split())
104
+
105
+
106
+ def digest(value: str) -> str:
107
+ return hashlib.sha256(value.encode()).hexdigest()
108
+
109
+
110
+ def ranking(value: str, seed: int) -> str:
111
+ return digest(f"{seed}:{value}")
112
+
113
+
114
+ def download(url: str, path: Path) -> None:
115
+ if path.exists():
116
+ return
117
+ path.parent.mkdir(parents=True, exist_ok=True)
118
+ temporary = path.with_suffix(path.suffix + ".part")
119
+ with httpx.stream("GET", url, follow_redirects=True, timeout=180) as response:
120
+ response.raise_for_status()
121
+ with temporary.open("wb") as stream:
122
+ for chunk in response.iter_bytes(1024 * 1024):
123
+ stream.write(chunk)
124
+ temporary.replace(path)
125
+
126
+
127
+ def checkout(path: Path | None, raw: Path) -> Path:
128
+ if path is not None:
129
+ if (path / ".revision").read_text().strip() != NIMBLE_REVISION:
130
+ raise ValueError("The supplied Nimble checkout is not the pinned revision")
131
+ return path.resolve()
132
+ path = raw / "nimble-source"
133
+ if not path.exists():
134
+ archive = raw / f"nimble-{NIMBLE_REVISION}.tar.gz"
135
+ download(f"https://codeload.github.com/bespokelabsai/nimble/tar.gz/{NIMBLE_REVISION}", archive)
136
+ with tarfile.open(archive) as bundle:
137
+ bundle.extractall(raw, filter="data")
138
+ (raw / f"nimble-{NIMBLE_REVISION}").rename(path)
139
+ (path / ".revision").write_text(NIMBLE_REVISION + "\n")
140
+ return path.resolve()
141
+
142
+
143
+ class RawData:
144
+ def __init__(self, args: Arguments) -> None:
145
+ self.cache = args.cache_root
146
+ self.supplied = args.source_cache
147
+ self.folder = args.output / "raw"
148
+ self.files: dict[str, dict[str, str]] = {}
149
+
150
+ def remember(self, path: Path, source: str) -> None:
151
+ key = f"{source}/{path.name}"
152
+ if key not in self.files:
153
+ self.files[key] = {"name": path.name, "sha256": checksum(path)}
154
+
155
+ def archive(self, source: str) -> Path:
156
+ url, name = ARCHIVES[source]
157
+ cached = self.supplied / name if self.supplied else self.folder / name
158
+ if not cached.exists():
159
+ cached = self.folder / name
160
+ download(url, cached)
161
+ self.remember(cached, source)
162
+ if self.files[f"{source}/{cached.name}"]["sha256"] != ARCHIVE_SHA256[source]:
163
+ raise ValueError(f"The {source} archive differs from the pinned checksum")
164
+ return cached
165
+
166
+ def rows(self, source: str, split: str, locale: str = "") -> Iterator[dict[str, JSONValue]]:
167
+ if source == "massive":
168
+ with tarfile.open(self.archive(source)) as archive:
169
+ member = next(m for m in archive.getmembers() if m.name.endswith(f"/data/{locale}.jsonl"))
170
+ stream = archive.extractfile(member)
171
+ assert stream is not None
172
+ for line in stream:
173
+ row = cast(dict[str, JSONValue], json.loads(line))
174
+ if row["partition"] == split:
175
+ yield row
176
+ return
177
+ if source in ("vitaminc", "multinli"):
178
+ member_name = f"vitaminc/{split}.jsonl" if source == "vitaminc" else f"multinli_1.0/multinli_1.0_{split}.jsonl"
179
+ with zipfile.ZipFile(self.archive(source)) as zip_archive, zip_archive.open(member_name) as zip_stream:
180
+ for line in zip_stream:
181
+ yield cast(dict[str, JSONValue], json.loads(line))
182
+ return
183
+ cached = self.supplied / source / f"{split}.jsonl" if self.supplied else self.folder / source / f"{split}.jsonl"
184
+ if cached.exists():
185
+ self.remember(cached, source)
186
+ with cached.open() as text_stream:
187
+ for index, text_line in enumerate(text_stream):
188
+ row = cast(dict[str, JSONValue], json.loads(text_line))
189
+ if source == "civil_comments":
190
+ row["row_index"] = index
191
+ yield row
192
+ return
193
+ from datasets import Dataset, load_dataset
194
+
195
+ spec = SOURCES[source]
196
+ paths = sorted((self.cache / "datasets").glob(f"**/{spec.revision}/*-{split}*.arrow"))
197
+ datasets: list[Dataset]
198
+ if paths:
199
+ datasets = [Dataset.from_file(str(path)) for path in paths]
200
+ for path in paths:
201
+ self.remember(path, source)
202
+ else:
203
+ datasets = [cast(Dataset, load_dataset(spec.repository, spec.config, split=split,
204
+ revision=spec.revision, cache_dir=str(self.folder / "hf")))]
205
+ index = 0
206
+ for dataset in datasets:
207
+ for value in dataset:
208
+ row = cast(dict[str, JSONValue], value)
209
+ if source == "civil_comments":
210
+ row["row_index"] = index
211
+ index += 1
212
+ yield row
213
+
214
+
215
+ class Families:
216
+ """Conservative PAWS groups: shared sentences or identical token bags connect."""
217
+
218
+ def __init__(self, raw: RawData) -> None:
219
+ self.parents: dict[str, str] = {}
220
+ self.paws: dict[tuple[str, str], str] = {}
221
+ for split in ("train", "validation", "test"):
222
+ for row in raw.rows("paws", split):
223
+ values: list[str] = []
224
+ for key in ("sentence1", "sentence2"):
225
+ text = normalized(str(row[key]))
226
+ values.extend((digest(text), digest(" ".join(sorted(re.findall(r"\w+", text))))))
227
+ roots = [self.find(value) for value in values]
228
+ root = min(roots)
229
+ for other in roots:
230
+ self.parents[other] = root
231
+ self.paws[split, str(row["id"])] = root
232
+
233
+ def find(self, value: str) -> str:
234
+ self.parents.setdefault(value, value)
235
+ while value != self.parents[value]:
236
+ self.parents[value] = self.parents[self.parents[value]]
237
+ value = self.parents[value]
238
+ return value
239
+
240
+ def key(self, source: str, split: str, raw: dict[str, JSONValue]) -> str:
241
+ if source == "paws":
242
+ return self.find(self.paws[split, str(raw["id"])])
243
+ field = {"massive": "id", "vitaminc": "case_id", "multinli": "promptID", "pubmedqa": "pubid"}.get(source)
244
+ if field:
245
+ return str(raw[field])
246
+ field = {"boolq": "passage", "squad2": "context", "aegis2": "prompt", "civil_comments": "text"}[source]
247
+ return digest(normalized(str(raw[field])))
248
+
249
+
250
+ def task_source(task: str) -> tuple[str, str]:
251
+ if task.startswith("massive-"):
252
+ return "massive", task.removeprefix("massive-")
253
+ return ("vitaminc", "") if task == "vitaminc-dev" else (task, "")
254
+
255
+
256
+ def convert(raw: dict[str, JSONValue], task: str, split: str, converter: Converter,
257
+ families: Families) -> tuple[Example, dict[str, JSONValue]] | None:
258
+ source, locale = task_source(task)
259
+ value = converter.record(raw | {"partition": "test"} if source == "massive" else raw, locale)
260
+ if value is None:
261
+ return None
262
+ model_input = cast(dict[str, JSONValue], value["input"])
263
+ questions = cast(dict[str, Question], model_input["questions"])
264
+ question = questions["decision"]
265
+ reference = cast(dict[str, JSONValue], value["reference"])
266
+ label = cast(Label, reference["target"])
267
+ target: Target = label
268
+ distribution = reference.get("distribution")
269
+ if split == "train" and source != "pubmedqa" and isinstance(distribution, dict):
270
+ if question["type"] == "noul":
271
+ target = float(cast(float, distribution["true"]))
272
+ elif question["type"] == "choice":
273
+ target = [float(cast(float, distribution[key])) for key in question["criteria"]]
274
+ provenance: dict[str, JSONValue] = {
275
+ "dataset": source, "split": split, "upstream_id": value["id"], "url": converter.SOURCE_URL,
276
+ "license": converter.LICENSE, "nimble_revision": NIMBLE_REVISION,
277
+ }
278
+ if source in SOURCES:
279
+ provenance["revision"] = SOURCES[source].revision
280
+ if distribution is not None:
281
+ provenance["human_distribution"] = distribution
282
+ identifier = f"{task}:{split}:{value['id']}"
283
+ if split == "train" and source != "pubmedqa":
284
+ # Some upstream training IDs name several distinct sentence pairs.
285
+ identifier += ":" + digest(json.dumps(model_input["state"], sort_keys=True, ensure_ascii=False))[:16]
286
+ result: Example = {
287
+ "id": identifier, "suite": task,
288
+ "family": families.key(source, split, raw), "state": cast(Content, model_input["state"]),
289
+ "question": question, "target": target, "label": label, "source": provenance,
290
+ }
291
+ return result, value
292
+
293
+
294
+ def choose(rows: list[Example], quota: int, seed: int, excluded: set[str] | None = None) -> list[Example]:
295
+ groups: dict[str, list[Example]] = defaultdict(list)
296
+ for row in rows:
297
+ if excluded is None or row["family"] not in excluded:
298
+ groups[row["family"]].append(row)
299
+ selected: list[Example] = []
300
+ for family in sorted(groups, key=lambda key: ranking(key, seed)):
301
+ group = groups[family]
302
+ if len(selected) + len(group) <= quota:
303
+ selected.extend(sorted(group, key=lambda row: row["id"]))
304
+ return selected
305
+
306
+
307
+ def text_parts(row: Example) -> list[str]:
308
+ state = row["state"]
309
+ if isinstance(state, str):
310
+ return [state]
311
+ if isinstance(state, dict):
312
+ return [value for value in state.values() if isinstance(value, str)]
313
+ return [json.dumps(state, sort_keys=True)]
314
+
315
+
316
+ def state_key(row: Example) -> str:
317
+ return digest(normalized(json.dumps(row["state"], sort_keys=True, ensure_ascii=False)))
318
+
319
+
320
+ class NearDuplicates:
321
+ """Candidate shingle index followed by exact Jaccard checks; no label access."""
322
+
323
+ def __init__(self) -> None:
324
+ self.parts: list[set[str]] = []
325
+ self.index: dict[str, list[int]] = defaultdict(list)
326
+
327
+ @staticmethod
328
+ def shingles(text: str) -> set[str]:
329
+ words = re.findall(r"\w+", normalized(text))
330
+ return {" ".join(words[index:index + 5]) for index in range(len(words) - 4)} if len(words) >= 20 else set()
331
+
332
+ def add(self, row: Example) -> None:
333
+ for text in text_parts(row):
334
+ values = self.shingles(text)
335
+ if values:
336
+ index = len(self.parts)
337
+ self.parts.append(values)
338
+ for anchor in sorted(values, key=digest)[:8]:
339
+ self.index[anchor].append(index)
340
+
341
+ def matches(self, row: Example) -> bool:
342
+ for text in text_parts(row):
343
+ values = self.shingles(text)
344
+ if not values:
345
+ continue
346
+ candidates = {index for anchor in sorted(values, key=digest)[:8] for index in self.index.get(anchor, [])}
347
+ for index in candidates:
348
+ other = self.parts[index]
349
+ if len(values & other) / len(values | other) >= 0.85:
350
+ return True
351
+ return False
352
+
353
+
354
+ def write_rows(path: Path, rows: list[Example]) -> str:
355
+ payload = "".join(json.dumps(row, ensure_ascii=True, allow_nan=False) + "\n" for row in rows)
356
+ if path.exists() and path.read_text() != payload:
357
+ raise FileExistsError(f"Refusing to replace a different frozen dataset: {path}")
358
+ path.parent.mkdir(parents=True, exist_ok=True)
359
+ path.write_text(payload)
360
+ return checksum(path)
361
+
362
+
363
+ def images(count: int, split: str, seed: int, output: Path) -> list[Example]:
364
+ rng = random.Random(seed)
365
+ colors = {"red": "#df3030", "blue": "#3068da", "green": "#259951", "orange": "#ed911d"}
366
+ shapes = ("circle", "square", "triangle")
367
+ rows: list[Example] = []
368
+ directory = output / "images" / split
369
+ directory.mkdir(parents=True, exist_ok=True)
370
+ for scene in range(math.ceil(count / 4)):
371
+ picture = Image.new("RGB", (224, 224), "#f9f6ee")
372
+ drawing = ImageDraw.Draw(picture)
373
+ objects: list[tuple[str, str]] = []
374
+ for x in (55, 169):
375
+ color, shape = rng.choice(list(colors)), rng.choice(shapes)
376
+ y, radius = rng.randint(85, 135), rng.randint(25, 40)
377
+ box = (x - radius, y - radius, x + radius, y + radius)
378
+ if shape == "circle":
379
+ drawing.ellipse(box, fill=colors[color])
380
+ elif shape == "square":
381
+ drawing.rectangle(box, fill=colors[color])
382
+ else:
383
+ drawing.polygon(((x, y - radius), (x + radius, y + radius), (x - radius, y + radius)), fill=colors[color])
384
+ objects.append((color, shape))
385
+ image_path = directory / f"{scene:04}.png"
386
+ picture.save(image_path)
387
+ for side, (color, shape) in zip(("left", "right"), objects):
388
+ for attribute, label, options in (("color", color, list(colors)), ("shape", shape, list(shapes))):
389
+ rows.append({"id": f"images:{split}:{scene}:{side}:{attribute}", "suite": "image_retention",
390
+ "family": f"{split}:{scene}", "state": "Use the picture to answer the question.",
391
+ "images": [str(image_path)], "label": label, "target": label,
392
+ "question": {"type": "choice", "instructions": f"What is the {attribute} of the {side} object?",
393
+ "criteria": dict.fromkeys(options)},
394
+ "source": {"dataset": "generated_geometry", "split": split,
395
+ "seed": seed, "image_sha256": checksum(image_path), "license": "Apache-2.0"}})
396
+ return rows[:count]
397
+
398
+
399
+ def validate(rows: list[Example]) -> None:
400
+ identifiers: set[str] = set()
401
+ for row in rows:
402
+ if row["id"] in identifiers:
403
+ raise ValueError(f"Duplicate example ID: {row['id']}")
404
+ identifiers.add(row["id"])
405
+ question, target, label = row["question"], row["target"], row["label"]
406
+ if not row["family"]:
407
+ raise ValueError("Missing family")
408
+ if question["type"] == "choice":
409
+ options = list(question["criteria"])
410
+ if not isinstance(label, str) or label not in options:
411
+ raise ValueError("Invalid Choice hard label")
412
+ if isinstance(target, list):
413
+ if len(target) != len(options) or abs(sum(target) - 1) > 1e-6 or any(not math.isfinite(p) or p < 0 for p in target):
414
+ raise ValueError("Invalid soft Choice target")
415
+ elif target not in options:
416
+ raise ValueError("Invalid Choice target")
417
+ elif question["type"] == "noul":
418
+ if type(label) is not bool or not isinstance(target, (bool, float)) or not 0 <= target <= 1:
419
+ raise ValueError("Invalid Noul label/target")
420
+ elif question["type"] == "score": # team-1: ordered rubric, label = level index as a string ("0".."n-1")
421
+ levels = [str(i) for i in range(len(question["criteria"]))]
422
+ if not isinstance(label, str) or label not in levels:
423
+ raise ValueError("Invalid Score hard label")
424
+ if isinstance(target, list):
425
+ if len(target) != len(levels) or abs(sum(target) - 1) > 1e-6 or any(not math.isfinite(p) or p < 0 for p in target):
426
+ raise ValueError("Invalid soft Score target")
427
+ elif target not in levels:
428
+ raise ValueError("Invalid Score target")
429
+ else:
430
+ raise ValueError("The accuracy experiment admits only Choice, Noul and Score examples")
431
+
432
+
433
+ def token_lengths(rows: list[Example], cache: Path) -> None:
434
+ """Measure the exact shared prompt with the processor, without model weights."""
435
+ from transformers import AutoProcessor
436
+ from transformers.models.qwen3_vl.processing_qwen3_vl import Qwen3VLProcessor
437
+ from autojev.model import BASE_MODEL, BASE_REVISION, decision_messages, open_image
438
+
439
+ processor = cast(Qwen3VLProcessor, AutoProcessor.from_pretrained(
440
+ BASE_MODEL, revision=BASE_REVISION, cache_dir=str(cache / "hub")))
441
+ processor.image_processor.size = {"shortest_edge": 65536, "longest_edge": 262144}
442
+ codes = [code for code in string.ascii_uppercase if len(processor.tokenizer.encode(code, add_special_tokens=False)) == 1]
443
+ if len(codes) != 26:
444
+ raise ValueError("The benchmark requires the model's first 26 answer codes")
445
+ text_rows = [row for row in rows if not row.get("images")]
446
+ for start in range(0, len(text_rows), 512):
447
+ batch = text_rows[start:start + 512]
448
+ texts = [processor.apply_chat_template(decision_messages(row, codes), tokenize=False, # type: ignore[arg-type]
449
+ add_generation_prompt=True, enable_thinking=False) for row in batch]
450
+ encoded = cast(list[list[int]], processor.tokenizer(texts, padding=False, truncation=False)["input_ids"])
451
+ for row, tokens in zip(batch, encoded):
452
+ row["source"]["input_tokens"] = len(tokens)
453
+ for row in rows:
454
+ if not row.get("images"):
455
+ continue
456
+ text = processor.apply_chat_template(decision_messages(row, codes), tokenize=False, # type: ignore[arg-type]
457
+ add_generation_prompt=True, enable_thinking=False)
458
+ encoded_images = processor(text=[text], images=[open_image(image) for image in row.get("images", [])])
459
+ row["source"]["input_tokens"] = len(cast(list[list[int]], encoded_images["input_ids"])[0])
460
+
461
+
462
+ def build(args: Arguments) -> None:
463
+ args.output.mkdir(parents=True, exist_ok=True)
464
+ raw = RawData(args)
465
+ dependency = checkout(args.nimble_source, raw.folder)
466
+ sys.path.insert(0, str(dependency))
467
+ serializer = cast(Serializer, importlib.import_module("nimble.datasets.public_benchmarks"))
468
+ families = Families(raw)
469
+ folds: dict[str, list[Example]] = {name: [] for name in ("known", "test", "dev", "temperature", "train")}
470
+ report: dict[str, object] = {"seed": args.seed, "nimble_revision": NIMBLE_REVISION, "tasks": {}, "sources": {}}
471
+ task_reports = cast(dict[str, object], report["tasks"])
472
+ source_reports = cast(dict[str, object], report["sources"])
473
+ protected: dict[str, set[str]] = defaultdict(set)
474
+ record("data_started", seed=args.seed, tasks=list(TASKS))
475
+ for task in TASKS:
476
+ source, locale = task_source(task)
477
+ converter = cast(Converter, importlib.import_module(f"nimble.datasets.public_sources.{source}"))
478
+ published = json.loads((dependency / f"docs/assets/public-benchmarks/subsets/{task}-manifest.json").read_text())
479
+ known_ids = set(published["ids"])
480
+ pools: dict[str, list[Example]] = defaultdict(list)
481
+ exact_known: list[dict[str, JSONValue]] = []
482
+ stats: Counter[str] = Counter()
483
+ for split in sorted({KNOWN_SPLITS[source], FINAL_SPLITS[source], DEVELOPMENT_SPLITS[source]}):
484
+ for value in raw.rows(source, split, locale):
485
+ result = convert(value, task, split, converter, families)
486
+ if result is None:
487
+ stats["invalid_heldout_rows"] += 1
488
+ continue
489
+ row, upstream = result
490
+ protected[source].add(row["family"])
491
+ if split == KNOWN_SPLITS[source] and upstream["id"] in known_ids:
492
+ folds["known"].append(row)
493
+ exact_known.append(upstream)
494
+ pools[split].append(row)
495
+ unique_ids: dict[str, Example] = {}
496
+ for candidate in pools[split]:
497
+ previous_row = unique_ids.get(candidate["id"])
498
+ if previous_row is not None:
499
+ if previous_row["state"] != candidate["state"] or previous_row["label"] != candidate["label"]:
500
+ raise ValueError("Conflicting rows share an upstream example ID")
501
+ stats["duplicate_heldout_ids_removed"] += 1
502
+ unique_ids.setdefault(candidate["id"], candidate)
503
+ pools[split] = list(unique_ids.values())
504
+ known_payload = "".join(serializer.jsonl_line(row) for row in sorted(exact_known, key=lambda row: str(row["id"])))
505
+ if digest(known_payload) != published["dataset_sha256"]:
506
+ raise ValueError(f"Known benchmark content changed for {task}")
507
+ # Stronger grouping can connect known rows to extra sibling examples.
508
+ excluded = {row["family"] for row in folds["known"] if row["source"]["dataset"] == source}
509
+ for fold, quota, split in (("test", TEST_QUOTAS[task], FINAL_SPLITS[source]),
510
+ ("dev", DEV_QUOTAS[task], DEVELOPMENT_SPLITS[source]),
511
+ ("temperature", TEMPERATURE_QUOTAS[task], DEVELOPMENT_SPLITS[source])):
512
+ # MASSIVE locales must receive exactly the same family IDs.
513
+ if locale == "de-DE":
514
+ wanted = {row["family"] for row in folds[fold] if row["suite"] == "massive-en-US"}
515
+ selected = [row for row in pools[split] if row["family"] in wanted]
516
+ if len(selected) != len(wanted):
517
+ raise ValueError("A MASSIVE translation is missing")
518
+ else:
519
+ selected = choose(pools[split], quota, args.seed + {"test": 1, "dev": 2, "temperature": 3}[fold], excluded)
520
+ folds[fold].extend(selected)
521
+ excluded.update(row["family"] for row in selected)
522
+ stats[fold] = len(selected)
523
+ stats["known"] = len(exact_known)
524
+ source_reports[source] = {"url": converter.SOURCE_URL, "license": converter.LICENSE,
525
+ "revision": SOURCES[source].revision if source in SOURCES else None,
526
+ "converter_sha256": checksum(dependency / f"nimble/datasets/public_sources/{source}.py")}
527
+ task_reports[task] = dict(stats)
528
+ record("data_task_partitioned", task=task, counts=dict(stats))
529
+
530
+ # Keep folds disjoint even when repeated text carries different upstream IDs.
531
+ protected_states: set[str] = set()
532
+ protected_near = NearDuplicates()
533
+ for fold in ("known", "test", "dev", "temperature"):
534
+ if fold != "known":
535
+ bad_families = {(str(row["source"]["dataset"]), row["family"]) for row in folds[fold]
536
+ if state_key(row) in protected_states or protected_near.matches(row)}
537
+ folds[fold] = [row for row in folds[fold] if (str(row["source"]["dataset"]), row["family"]) not in bad_families]
538
+ report[f"{fold}_duplicate_families_removed"] = len(bad_families)
539
+ for row in folds[fold]:
540
+ protected_states.add(state_key(row))
541
+ protected_near.add(row)
542
+
543
+ token_lengths([row for name, rows in folds.items() if name != "train" for row in rows], args.cache_root)
544
+ early_partitions: dict[str, object] = {}
545
+ for name in ("known", "test", "dev", "temperature"):
546
+ rows = folds[name]
547
+ overlong = {(str(row["source"]["dataset"]), row["family"]) for row in rows
548
+ if cast(int, row["source"]["input_tokens"]) > args.max_length}
549
+ if name == "known" and overlong:
550
+ raise ValueError("The known benchmark exceeds the token limit; never silently change its records")
551
+ folds[name] = [row for row in rows if (str(row["source"]["dataset"]), row["family"]) not in overlong]
552
+ report[f"{name}_overlong_families_excluded"] = len(overlong)
553
+ validate(folds[name])
554
+ early_partitions[name] = {"rows": len(folds[name]), "sha256": write_rows(args.output / f"{name}.jsonl", folds[name]),
555
+ "tasks": dict(Counter(row["suite"] for row in folds[name]))}
556
+ args.manifest.parent.mkdir(parents=True, exist_ok=True)
557
+ (args.manifest.parent / "evaluation-partitions.json").write_text(json.dumps(early_partitions, indent=2) + "\n")
558
+ record("evaluation_data_frozen", partitions=early_partitions)
559
+
560
+ # Training-family sampling is label-blind. Soft human annotations are targets
561
+ # only for sources that actually supply them; teacher confidence is not used.
562
+ for task in TASKS:
563
+ quota = int(TRAIN_QUOTAS[task] * args.train_scale)
564
+ if not quota:
565
+ continue
566
+ source, locale = task_source(task)
567
+ converter = cast(Converter, importlib.import_module(f"nimble.datasets.public_sources.{source}"))
568
+ candidates: list[Example] = []
569
+ stats = Counter[str]()
570
+ pool_fraction = min(1.0, quota * 5 / {"vitaminc": 370653, "multinli": 392702, "civil_comments": 1804874}.get(source, 1))
571
+ for value in raw.rows(source, "train", locale):
572
+ family = families.key(source, "train", value)
573
+ if family in protected[source]:
574
+ stats["heldout_family_rows_excluded"] += 1
575
+ continue
576
+ if int(ranking(family, args.seed), 16) / 2**256 >= pool_fraction:
577
+ continue
578
+ result = convert(value, task, "train", converter, families)
579
+ if result is None:
580
+ stats["invalid_training_rows"] += 1
581
+ continue
582
+ row, _ = result
583
+ candidates.append(row)
584
+ bad = {row["family"] for row in candidates
585
+ if state_key(row) in protected_states or protected_near.matches(row)}
586
+ candidates = [row for row in candidates if row["family"] not in bad]
587
+ unique: dict[str, Example] = {}
588
+ conflicts: set[str] = set()
589
+ for row in candidates:
590
+ key = state_key(row)
591
+ if key in unique and unique[key]["label"] != row["label"]:
592
+ conflicts.add(key)
593
+ unique.setdefault(key, row)
594
+ candidates = [row for key, row in unique.items() if key not in conflicts]
595
+ selected = choose(candidates, quota, args.seed + 4)
596
+ if len(selected) < 0.8 * quota:
597
+ raise ValueError(f"Too few training rows for {task}: {len(selected)} / {quota}")
598
+ folds["train"].extend(selected)
599
+ stats.update({"train": len(selected), "near_duplicate_families_excluded": len(bad),
600
+ "conflicting_state_labels_excluded": len(conflicts)})
601
+ cast(dict[str, object], task_reports[task]).update(dict(stats))
602
+ record("data_training_task", task=task, counts=dict(stats))
603
+
604
+ folds["train"].extend(images(args.image_train, "train", args.seed + 100, args.output))
605
+ folds["image_test"] = images(args.image_eval, "test", args.seed + 101, args.output)
606
+ token_lengths(folds["train"] + folds["image_test"], args.cache_root)
607
+ for name in ("train", "image_test"):
608
+ rows = folds[name]
609
+ overlong = {(str(row["source"]["dataset"]), row["family"]) for row in rows
610
+ if cast(int, row["source"]["input_tokens"]) > args.max_length}
611
+ if name == "known" and overlong:
612
+ raise ValueError("The known benchmark exceeds the model token limit; never silently change its records")
613
+ folds[name] = [row for row in rows if (str(row["source"]["dataset"]), row["family"]) not in overlong]
614
+ report[f"{name}_overlong_families_excluded"] = len(overlong)
615
+ random.Random(args.seed).shuffle(folds["train"])
616
+ partitions: dict[str, object] = {}
617
+ previous: dict[tuple[str, str], str] = {}
618
+ for name, rows in folds.items():
619
+ validate(rows)
620
+ # Known contains fixed upstream rows; sibling locales are one family.
621
+ keys = {(str(row["source"]["dataset"]), row["family"]) for row in rows}
622
+ for family_key in keys:
623
+ if family_key in previous:
624
+ raise ValueError(f"Family crossed {previous[family_key]} and {name}")
625
+ previous[family_key] = name
626
+ path = args.output / f"{name}.jsonl"
627
+ partitions[name] = {"rows": len(rows), "families": len(keys), "sha256": write_rows(path, rows),
628
+ "tasks": dict(Counter(row["suite"] for row in rows)),
629
+ "tokens": sum(cast(int, row["source"]["input_tokens"]) for row in rows),
630
+ "maximum_input_tokens": max(cast(int, row["source"]["input_tokens"]) for row in rows),
631
+ "question_types": dict(Counter(row["question"]["type"] for row in rows))}
632
+ identifiers = [{"id": row["id"], "dataset": row["source"]["dataset"], "family": row["family"]} for row in rows]
633
+ identity_path = args.manifest.parent / "splits" / f"{name}.json"
634
+ identity_path.parent.mkdir(parents=True, exist_ok=True)
635
+ identity_path.write_text(json.dumps(identifiers, separators=(",", ":")) + "\n")
636
+ report.update({"partitions": partitions, "raw_files": raw.files, "generator_sha256": checksum(Path(__file__)),
637
+ "quotas": {"train": TRAIN_QUOTAS, "test": TEST_QUOTAS, "dev": DEV_QUOTAS, "temperature": TEMPERATURE_QUOTAS},
638
+ "max_input_tokens": args.max_length, "train_scale": args.train_scale,
639
+ "family_policy": "Whole source families; shared MASSIVE IDs; normalized passages/prompts; PAWS connected sentence/token-bag components.",
640
+ "near_duplicate_policy": "Five-word shingle Jaccard >= 0.85, candidates retrieved by eight minimum SHA256 anchors; text parts below 20 words use exact checks only.",
641
+ "known_suite": "The 3247 original Choice/Noul records are verified against pinned Nimble checksums before adding provenance.",
642
+ "limitations": ["PAWS original generation mapping is unavailable (official URL returned 403); sentence/token-bag components and shingle checks are heuristic.",
643
+ "Near-duplicate retrieval is approximate and does not prove absence of every paraphrase or pretraining overlap.",
644
+ "PubMedQA labeled data are reserved for evaluation; no in-domain PubMedQA training in the first corpus.",
645
+ "Image retention is controlled geometry, not a general vision benchmark.",
646
+ "Fresh means unused in this project's tuning, not necessarily unseen by the base model or Jev."]})
647
+ args.manifest.parent.mkdir(parents=True, exist_ok=True)
648
+ args.manifest.write_text(json.dumps(report, indent=2) + "\n")
649
+ record("data_frozen", manifest_sha256=checksum(args.manifest), partitions=partitions)
650
+ print(json.dumps(partitions, indent=2), flush=True)
651
+
652
+
653
+ def main() -> None:
654
+ parser = argparse.ArgumentParser(description=__doc__)
655
+ parser.add_argument("--cache-root", type=Path, default=Path.home() / ".cache/huggingface")
656
+ parser.add_argument("--source-cache", type=Path)
657
+ parser.add_argument("--nimble-source", type=Path)
658
+ parser.add_argument("--output", type=Path, default=Path("data"))
659
+ parser.add_argument("--manifest", type=Path, default=Path("progress/data-manifest.json"))
660
+ parser.add_argument("--seed", type=int, default=SEED)
661
+ parser.add_argument("--max-length", type=int, default=8192)
662
+ parser.add_argument("--train-scale", type=float, default=1.0)
663
+ parser.add_argument("--image-train", type=int, default=1000)
664
+ parser.add_argument("--image-eval", type=int, default=200)
665
+ build(parser.parse_args(namespace=Arguments()))
666
+
667
+
668
+ if __name__ == "__main__":
669
+ main()