JEBS-4B / src /mdtrain /data.py
the-Xy's picture
JEBS-4B v3: sealed artifact, runtime, model card
6592c2d verified
Raw History Blame Contribute Delete
7.72 kB
"""Strict prepared readers and explicit without-replacement question orders."""
import collections
import json
import random
from dataclasses import dataclass
from pathlib import Path
from mdprep.common import sha256_file
from mdprep.native import digest_json, validate_contract
from mdprep.records import TrainingExample, parse_record
from mdprep.verify import validate_verification_contract
EVALUATION_SPLITS = ("dev", "calibration", "policy", "test_id", "test_ood")
DEFAULT_SCHEDULE = "paired"
@dataclass(frozen=True)
class TrainingSchedule:
name: str
phase_components: tuple[tuple[str, tuple[str, ...]], ...]
@property
def phases(self) -> tuple[str, ...]:
return tuple(phase for phase, _ in self.phase_components)
def compose(self, components: dict[str, list[str]]) -> dict[str, list[str]]:
orders = {}
for phase, names in self.phase_components:
order = []
for name in names:
order.extend(components[name])
orders[phase] = order
return orders
SCHEDULES = {
"paired": TrainingSchedule("paired", (("head", ("warmup",)), ("joint", ("training",)))),
"pointer-only": TrainingSchedule("pointer-only", (("head", ("warmup", "training")),)),
}
OPTIMIZER_PHASES = tuple(dict.fromkeys(
phase for schedule in SCHEDULES.values() for phase in schedule.phases))
def training_schedule(name: str) -> TrainingSchedule:
try:
return SCHEDULES[name]
except KeyError as error:
raise ValueError(f"Unknown training schedule: {name}") from error
def bound_schedule(payload: dict) -> str:
"""Resolve historical paired identities while requiring new schedules to name themselves."""
name = payload.get("schedule", DEFAULT_SCHEDULE)
training_schedule(name)
return name
def qualified_inputs(root: Path, *, processor: dict | None = None,
verification: dict | None = None) -> dict:
"""Commit successful qualification, processor implementation, rows, and media."""
processor = processor if processor is not None else validate_contract(root)
verification = verification if verification is not None else validate_verification_contract(root)
for split in set(processor["prepared_hashes"]) & set(verification["prepared_hashes"]):
if processor["prepared_hashes"][split] != verification["prepared_hashes"][split]:
raise ValueError("Processor and data qualification disagree on prepared rows")
for relative in set(processor["media_hashes"]) & set(verification["media_hashes"]):
if processor["media_hashes"][relative] != verification["media_hashes"][relative]:
raise ValueError("Processor and data qualification disagree on media")
return {"processor_id": processor["processor_id"],
"processor_contract_id": digest_json(processor),
"qualification_id": verification["qualification_sha256"],
"prepared_hashes": processor["prepared_hashes"],
"media_hashes": processor["media_hashes"]}
@dataclass(frozen=True)
class DatasetView:
examples: tuple[TrainingExample, ...]
rows: tuple[dict, ...]
split: str
recipe: str
identity: str
@classmethod
def open(cls, root: Path, *, split: str, recipe: str = "mixed"):
if split not in ("train", *EVALUATION_SPLITS) or recipe not in ("real", "mixed"):
raise ValueError("Unknown dataset split or recipe")
processor = validate_contract(root)
verification = validate_verification_contract(root)
if split not in verification["scope"]:
raise ValueError(f"Verified scope does not include {split}")
if split not in processor["prepared_hashes"]:
raise ValueError(f"Processor scope does not include {split}")
path = root / "data/prepared" / f"{split}.jsonl"
rows, examples = [], []
with path.open() as stream:
for line in stream:
row = json.loads(line)
if row["split"] != split:
raise ValueError("Prepared row split mismatch")
example = parse_record(row)
if recipe == "real" and example.origin != "real_human_label":
continue
rows.append(row)
examples.append(example)
ids = [example.input.id for example in examples]
if not ids or len(ids) != len(set(ids)):
raise ValueError("Dataset view is empty or contains duplicate IDs")
prepared_hash = sha256_file(path)
if prepared_hash != processor["prepared_hashes"][split]:
raise ValueError("Prepared rows changed while constructing the qualified view")
media = {relative for example in examples
for relative in (*example.input.images, *(video.path for video in example.input.videos))}
commitment = qualified_inputs(root, processor=processor, verification=verification)
identity = digest_json({"prepared_sha256": prepared_hash, "recipe": recipe, "selected_ids": ids,
"media_hashes": {name: processor["media_hashes"][name] for name in sorted(media)},
"qualified_inputs": commitment})
return cls(tuple(examples), tuple(rows), split, recipe, identity)
def by_id(self) -> dict[str, TrainingExample]:
return {example.input.id: example for example in self.examples}
def ordered_ids(view: DatasetView, *, seed: int, lengths: dict[str, int]) -> list[str]:
rng = random.Random(seed)
strata = collections.defaultdict(lambda: collections.defaultdict(list))
for example, row in zip(view.examples, view.rows):
strata[(example.origin, row["modality"], example.input.kind)][lengths[example.input.id] // 256].append(example.input.id)
queues = []
for stratum in sorted(strata):
buckets = strata[stratum]
keys = sorted(buckets)
rng.shuffle(keys)
queue = []
for key in keys:
rng.shuffle(buckets[key])
queue.extend(buckets[key])
queues.append(collections.deque(queue))
rng.shuffle(queues)
result = []
while any(queues):
for queue in queues:
if queue:
result.append(queue.popleft())
if len(result) != len(view.examples) or len(set(result)) != len(result):
raise ValueError("Without-replacement order is not a permutation")
return result
def plan_orders(view: DatasetView, stage: str, lengths: dict[str, int],
schedule: str = DEFAULT_SCHEDULE) -> dict:
limits = {"diagnostic": (64, 8), "pilot": (1000, 16), "main": (len(view.examples), 100)}
if stage not in limits:
raise ValueError("Unknown training stage")
selected_schedule = training_schedule(schedule)
size, head_updates = limits[stage]
if len(view.examples) < size:
raise ValueError(f"{stage} requires {size} rows without recipe fallback")
order = ordered_ids(view, seed=1729, lengths=lengths)[:size]
warmup = [order[index % size] for index in range(head_updates * 16)]
training = ([order[index % size] for index in range(64 * 16)]
if stage == "diagnostic" else order)
phase_orders = selected_schedule.compose({"warmup": warmup, "training": training})
orders = {phase: phase_orders.get(phase, []) for phase in OPTIMIZER_PHASES}
updates = {phase: (len(orders[phase]) + 15) // 16 for phase in OPTIMIZER_PHASES}
return {**orders, "selection": order,
"head_updates": updates["head"], "joint_updates": updates["joint"],
"without_replacement_joint": stage != "diagnostic" and "joint" in selected_schedule.phases}