File size: 6,777 Bytes
63f41a6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | #!/usr/bin/env python
"""P2 integrator: assert the seams between the independently built pieces, on real files.
.venv/bin/python scripts/p2/check_seams.py keys --generations G.jsonl --verifications V.jsonl
.venv/bin/python scripts/p2/check_seams.py sft --generations G0 G1 --verifications V0 V1 --shards D/shards
Each check exists because the pieces were written by different hands against a prose contract,
and each failure below is silent when only one side is looked at:
keys generation -> verification. A verification file whose key set differs from the
generation file's (a sample never verified, a verdict for a sample that is not on disk, a
verdict at another cap or round) scores a different experiment than the one generated.
Also checks `code_sha256` against the generation on disk (a stale verdict).
sft verification -> trainer input. The tokens the trainer takes loss on must be EXACTLY the
`completion_ids` vLLM sampled for a sample the verifier ACCEPTED, and the context must be
exactly its `prompt_ids`. This re-derives the labels through the same `ShardDataset` +
`collate_sft` the trainer uses and does not trust `build_sft_data.py`'s own read-back
(an independent implementation of the same property is the only thing that catches a
shared misunderstanding).
Exit status 0 only if every check passes; a report JSON is printed either way.
"""
from __future__ import annotations
import argparse
import importlib.util
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO))
def _common():
if "p2_common" in sys.modules:
return sys.modules["p2_common"]
spec = importlib.util.spec_from_file_location("p2_common", Path(__file__).with_name("p2_common.py"))
mod = importlib.util.module_from_spec(spec)
sys.modules["p2_common"] = mod
spec.loader.exec_module(mod)
return mod
class SeamError(AssertionError):
pass
def _load(paths, key_fields):
out: dict[tuple, dict] = {}
for p in paths:
for rec in _common().iter_jsonl(p):
k = tuple(rec[f] for f in key_fields)
if k in out:
raise SeamError(f"duplicate key {k} in {p}")
out[k] = rec
return out
def check_keys(gen_paths, ver_paths) -> dict:
"""Generation key set == verification key set, and every verdict is for the code on disk."""
C = _common()
gens = _load(gen_paths, C.KEY_FIELDS)
vers = _load(ver_paths, C.KEY_FIELDS)
missing = sorted(set(gens) - set(vers))
extra = sorted(set(vers) - set(gens))
if missing or extra:
raise SeamError(f"key sets differ: {len(missing)} generated but unverified (e.g. {missing[:2]}), "
f"{len(extra)} verified but not generated (e.g. {extra[:2]})")
stale = [k for k, g in gens.items() if vers[k]["code_sha256"] != C.code_sha(g["lean_code"])]
if stale:
raise SeamError(f"{len(stale)} verdicts were computed for different code than the generation holds, e.g. {stale[:2]}")
# a length-capped sample is never a proof, whatever the verdict file says
bad = [k for k, g in gens.items() if g["finish_reason"] == "length" and vers[k]["accepted"]]
if bad:
raise SeamError(f"{len(bad)} length-truncated samples are marked accepted, e.g. {bad[:2]}")
rounds: dict = {}
for k, v in vers.items():
r = rounds.setdefault(k[2], [0, 0])
r[0] += 1
r[1] += int(bool(v["accepted"]))
return {"keys": len(gens), "by_round": {str(r): {"samples": a, "accepted": b} for r, (a, b) in sorted(rounds.items())}}
def check_sft(gen_paths, ver_paths, shards_dir) -> dict:
"""Every stored row's loss tokens == completion_ids of an ACCEPTED generation, context == prompt_ids."""
import numpy as np
from align.data.loader import collate_sft
from align.data.store import ShardDataset
C = _common()
gens = _load(gen_paths, C.KEY_FIELDS)
vers = _load(ver_paths, C.KEY_FIELDS)
rows = 0
kinds: dict = {}
seen: set = set()
for split in ("train", "held"):
d = Path(shards_dir) / "sft" / split
if not d.is_dir():
continue
ds = ShardDataset(d)
for i in range(len(ds)):
row = ds[i]
ex = row.extras
key = (row.problem_id, int(ex["sample_idx"]), int(ex["round"]), int(ex["cap_tokens"]))
if key not in gens:
raise SeamError(f"{split}[{i}] {key}: no such generation")
if not (key in vers and vers[key]["accepted"]):
raise SeamError(f"{split}[{i}] {key}: trained on a sample the verifier did not accept")
if key in seen:
raise SeamError(f"{key}: stored twice")
seen.add(key)
g = gens[key]
p = int(ex["prompt_len"])
ids = np.asarray(row.input_ids)
if p != len(g["prompt_ids"]) or not np.array_equal(ids[:p], np.asarray(g["prompt_ids"])):
raise SeamError(f"{key}: stored context is not the sampled prompt_ids")
lab = collate_sft([row], pad_token_id=0).labels[0].numpy()
trained = lab[lab != -100]
if not np.array_equal(trained, np.asarray(g["completion_ids"], dtype=trained.dtype)):
raise SeamError(f"{key}: tokens with loss ({len(trained)}) != completion_ids ({len(g['completion_ids'])})")
if not np.all(lab[:p] == -100):
raise SeamError(f"{key}: loss leaks into the prompt")
kinds[str(ex["kind"])] = kinds.get(str(ex["kind"]), 0) + 1
rows += 1
if rows == 0:
raise SeamError("no SFT rows were found; a vacuous pass")
accepted = sum(1 for v in vers.values() if v["accepted"])
return {"rows": rows, "by_kind": kinds, "accepted_samples": accepted, "accepted_not_used": accepted - rows}
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
sub = ap.add_subparsers(dest="cmd", required=True)
for name in ("keys", "sft"):
s = sub.add_parser(name)
s.add_argument("--generations", nargs="+", required=True)
s.add_argument("--verifications", nargs="+", required=True)
if name == "sft":
s.add_argument("--shards", required=True)
a = ap.parse_args(argv)
try:
rep = check_keys(a.generations, a.verifications) if a.cmd == "keys" else check_sft(a.generations, a.verifications, a.shards)
rep["passed"] = True
except SeamError as e:
rep = {"passed": False, "error": str(e)}
print(json.dumps(rep, indent=2))
return 0 if rep["passed"] else 1
if __name__ == "__main__":
raise SystemExit(main())
|