Upload build/audit_contamination.py with huggingface_hub
Browse files- build/audit_contamination.py +69 -14
build/audit_contamination.py
CHANGED
|
@@ -28,6 +28,7 @@
|
|
| 28 |
# Read-only over the mix. CPU session. No credentials in this file.
|
| 29 |
|
| 30 |
import argparse
|
|
|
|
| 31 |
import json
|
| 32 |
import os
|
| 33 |
import struct
|
|
@@ -93,6 +94,17 @@ def window_hashes(ids, k=K):
|
|
| 93 |
return h
|
| 94 |
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
def flatten(v, out):
|
| 97 |
if isinstance(v, str):
|
| 98 |
out.append(v)
|
|
@@ -212,8 +224,8 @@ def build_reference(tk, max_items_per_split):
|
|
| 212 |
else datasets.load_dataset(repo, split=sp, streaming=True))
|
| 213 |
n, g = ingest(tk, ds, max_items_per_split)
|
| 214 |
except Exception as e:
|
| 215 |
-
errors[ckey] =
|
| 216 |
-
print(f" ref {ckey}: UNREADABLE {
|
| 217 |
continue
|
| 218 |
if want and n < int(0.9 * want):
|
| 219 |
print(f" ref {ckey}: {n} of {want} rows via datasets -> rows-API fallback",
|
|
@@ -270,20 +282,23 @@ def per_task_counts(bits):
|
|
| 270 |
return [int((bits & np.uint16(1 << ti)).astype(np.uint16).sum()) for ti in range(len(REFS))]
|
| 271 |
|
| 272 |
|
| 273 |
-
def scan_docs(ref, refmask, docs):
|
| 274 |
"""Fold an iterator of (ordinal_within_source, raw_bytes) into counts.
|
| 275 |
|
| 276 |
The ordinal is the only identity a document ever gets here: a filter pass can drop it by index later
|
| 277 |
without anything in this process knowing what it said. `hit_docs_over_half` is the column that decides
|
| 278 |
interpretation -- contamination by verbatim inclusion shows up as documents whose grams are *mostly*
|
| 279 |
matched, while one shared boilerplate sentence shows up as a single gram in a handful of documents.
|
|
|
|
|
|
|
| 280 |
"""
|
| 281 |
import time
|
| 282 |
import hashlib
|
| 283 |
t0 = time.time()
|
| 284 |
s = {"docs": 0, "tokens": 0, "grams": 0, "hit_docs": 0, "hits": 0, "hit_docs_over_half": 0,
|
| 285 |
"hit_tokens": 0, "dup_docs": 0, "dup_tokens": 0, "per_task": [0] * len(REFS), "ordinals": []}
|
| 286 |
-
seen
|
|
|
|
| 287 |
names = [t[0] for t in REFS]
|
| 288 |
for idx, raw in docs:
|
| 289 |
b = bytes(raw)
|
|
@@ -381,7 +396,9 @@ def main():
|
|
| 381 |
json.dump(srv, open(os.path.join(a.ref_cache, "srv.json"), "w"))
|
| 382 |
print(f"reference cached under {a.ref_cache}", flush=True)
|
| 383 |
tasks_covered = sorted({k.split("::")[0] for k, v in refcounts.items()
|
| 384 |
-
if v >= MIN_ITEMS_PER_SPLIT}
|
|
|
|
|
|
|
| 385 |
print(f"reference: {ref.size:,} grams, {len(tasks_covered)}/8 tasks covered, "
|
| 386 |
f"{len(errors)} loader problems", flush=True)
|
| 387 |
|
|
@@ -397,7 +414,13 @@ def main():
|
|
| 397 |
for key in sorted(os.listdir(stage_root)):
|
| 398 |
sd = os.path.join(stage_root, key)
|
| 399 |
rd = os.path.join(sd, "record.json")
|
| 400 |
-
if not os.path.isdir(sd)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
continue
|
| 402 |
rec = json.load(open(rd))
|
| 403 |
paths = [os.path.join(a.root, s["file"]) for s in rec.get("shards", [])]
|
|
@@ -424,15 +447,25 @@ def main():
|
|
| 424 |
docs = grams_seen = mix_tokens = hit_docs = hit_docs_over_half = 0
|
| 425 |
dup_docs = dup_tokens = 0
|
| 426 |
missing = []
|
|
|
|
|
|
|
| 427 |
if os.path.exists(man_path):
|
| 428 |
man = json.load(open(man_path))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
for rec in man["shards"]:
|
| 430 |
p = os.path.join(a.root, rec["file"])
|
| 431 |
if not os.path.exists(p):
|
| 432 |
# A shard not on disk is unmeasured, not clean (E-017's shape).
|
| 433 |
missing.append(rec["file"])
|
| 434 |
continue
|
| 435 |
-
s = scan_docs(ref, refmask, doc_stream(BM, [p]))
|
| 436 |
docs += s["docs"]
|
| 437 |
mix_tokens += s["tokens"]
|
| 438 |
grams_seen += s["grams"]
|
|
@@ -442,21 +475,35 @@ def main():
|
|
| 442 |
dup_tokens += s["dup_tokens"]
|
| 443 |
for n, c in s["per_task_named"].items():
|
| 444 |
hits[n] = hits.get(n, 0) + c
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
else:
|
| 449 |
total_overlap = 0
|
| 450 |
print(f"no manifest at {a.root}: merged mix not scanned", flush=True)
|
| 451 |
|
| 452 |
-
|
|
|
|
| 453 |
noise_allow = max(10.0, 10.0 * expected_fp)
|
| 454 |
per_task_named = {t[0]: hits.get(t[0], 0) for t in REFS}
|
| 455 |
|
| 456 |
if a.write_filter:
|
| 457 |
os.makedirs(a.write_filter, exist_ok=True)
|
| 458 |
summary = {"k": K, "criterion": "any 13-gram of the document appears in a reference "
|
| 459 |
-
"train/validation/dev split",
|
|
|
|
| 460 |
for key, (ord_sorted, n_docs) in masks.items():
|
| 461 |
m = np.zeros(n_docs, dtype=np.uint8)
|
| 462 |
if ord_sorted.size:
|
|
@@ -488,6 +535,11 @@ def main():
|
|
| 488 |
"mix_grams_audited": grams_seen,
|
| 489 |
"mix_documents_with_any_hit": hit_docs,
|
| 490 |
"mix_documents_majority_hit": hit_docs_over_half,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
"overlap_by_task": per_task_named,
|
| 492 |
"overlap_total": total_overlap,
|
| 493 |
"by_source": by_source,
|
|
@@ -504,8 +556,11 @@ def main():
|
|
| 504 |
"mix_documents_majority_hit and source_hit_document_fraction to judge severity. "
|
| 505 |
"Duplicate counting is diagnostic only: de-duplication is out of scope (D-014), "
|
| 506 |
"contamination removal is not (§3.3)."),
|
| 507 |
-
"COVERED_ALL_SHARDS": bool(not missing and docs > 0),
|
| 508 |
-
"
|
|
|
|
|
|
|
|
|
|
| 509 |
}
|
| 510 |
report["AUDIT_PASSED"] = bool(report["MEASURED"] and total_overlap <= noise_allow)
|
| 511 |
txt = json.dumps(report, indent=1, default=str)
|
|
|
|
| 28 |
# Read-only over the mix. CPU session. No credentials in this file.
|
| 29 |
|
| 30 |
import argparse
|
| 31 |
+
import hashlib
|
| 32 |
import json
|
| 33 |
import os
|
| 34 |
import struct
|
|
|
|
| 94 |
return h
|
| 95 |
|
| 96 |
|
| 97 |
+
def brief_error(e):
|
| 98 |
+
"""Short, publishable description of a loader failure.
|
| 99 |
+
|
| 100 |
+
This report is shipped with the mix, and an exception string from a dataset reader can quote a cell of
|
| 101 |
+
benchmark text -- exactly the thing §3.3 keeps out of every artifact. Quoted spans are replaced.
|
| 102 |
+
"""
|
| 103 |
+
import re
|
| 104 |
+
msg = re.sub(r"'[^']*'|\"[^\"]*\"", "'…'", str(e).replace("\n", " "))
|
| 105 |
+
return f"{type(e).__name__}: {msg[:140]}"
|
| 106 |
+
|
| 107 |
+
|
| 108 |
def flatten(v, out):
|
| 109 |
if isinstance(v, str):
|
| 110 |
out.append(v)
|
|
|
|
| 224 |
else datasets.load_dataset(repo, split=sp, streaming=True))
|
| 225 |
n, g = ingest(tk, ds, max_items_per_split)
|
| 226 |
except Exception as e:
|
| 227 |
+
errors[ckey] = brief_error(e)
|
| 228 |
+
print(f" ref {ckey}: UNREADABLE {brief_error(e)}", flush=True)
|
| 229 |
continue
|
| 230 |
if want and n < int(0.9 * want):
|
| 231 |
print(f" ref {ckey}: {n} of {want} rows via datasets -> rows-API fallback",
|
|
|
|
| 282 |
return [int((bits & np.uint16(1 << ti)).astype(np.uint16).sum()) for ti in range(len(REFS))]
|
| 283 |
|
| 284 |
|
| 285 |
+
def scan_docs(ref, refmask, docs, seen=None):
|
| 286 |
"""Fold an iterator of (ordinal_within_source, raw_bytes) into counts.
|
| 287 |
|
| 288 |
The ordinal is the only identity a document ever gets here: a filter pass can drop it by index later
|
| 289 |
without anything in this process knowing what it said. `hit_docs_over_half` is the column that decides
|
| 290 |
interpretation -- contamination by verbatim inclusion shows up as documents whose grams are *mostly*
|
| 291 |
matched, while one shared boilerplate sentence shows up as a single gram in a handful of documents.
|
| 292 |
+
Pass one `seen` set across a whole mix, otherwise the duplicate count is per-shard and only a lower
|
| 293 |
+
bound (found in review).
|
| 294 |
"""
|
| 295 |
import time
|
| 296 |
import hashlib
|
| 297 |
t0 = time.time()
|
| 298 |
s = {"docs": 0, "tokens": 0, "grams": 0, "hit_docs": 0, "hits": 0, "hit_docs_over_half": 0,
|
| 299 |
"hit_tokens": 0, "dup_docs": 0, "dup_tokens": 0, "per_task": [0] * len(REFS), "ordinals": []}
|
| 300 |
+
if seen is None:
|
| 301 |
+
seen = set()
|
| 302 |
names = [t[0] for t in REFS]
|
| 303 |
for idx, raw in docs:
|
| 304 |
b = bytes(raw)
|
|
|
|
| 396 |
json.dump(srv, open(os.path.join(a.ref_cache, "srv.json"), "w"))
|
| 397 |
print(f"reference cached under {a.ref_cache}", flush=True)
|
| 398 |
tasks_covered = sorted({k.split("::")[0] for k, v in refcounts.items()
|
| 399 |
+
if v >= MIN_ITEMS_PER_SPLIT} -
|
| 400 |
+
{k.split("::")[0] for k in errors
|
| 401 |
+
if "::" in k and "[" not in k and not k.startswith("stage::")})
|
| 402 |
print(f"reference: {ref.size:,} grams, {len(tasks_covered)}/8 tasks covered, "
|
| 403 |
f"{len(errors)} loader problems", flush=True)
|
| 404 |
|
|
|
|
| 414 |
for key in sorted(os.listdir(stage_root)):
|
| 415 |
sd = os.path.join(stage_root, key)
|
| 416 |
rd = os.path.join(sd, "record.json")
|
| 417 |
+
if not os.path.isdir(sd):
|
| 418 |
+
continue
|
| 419 |
+
if not os.path.exists(rd):
|
| 420 |
+
# Absent record means the source never finished staging or was built without a Hub
|
| 421 |
+
# repo. Treating it as "nothing to scan" would merge it unfiltered (review finding).
|
| 422 |
+
errors[f"stage::{key}"] = "no record.json -- source is incomplete or unpublished"
|
| 423 |
+
print(f" stage {key}: NO record.json, source left unscanned", flush=True)
|
| 424 |
continue
|
| 425 |
rec = json.load(open(rd))
|
| 426 |
paths = [os.path.join(a.root, s["file"]) for s in rec.get("shards", [])]
|
|
|
|
| 447 |
docs = grams_seen = mix_tokens = hit_docs = hit_docs_over_half = 0
|
| 448 |
dup_docs = dup_tokens = 0
|
| 449 |
missing = []
|
| 450 |
+
val = {"docs": 0, "tokens": 0, "grams": 0, "hit_docs": 0, "hits": 0, "hit_docs_over_half": 0}
|
| 451 |
+
manifest_digest = None
|
| 452 |
if os.path.exists(man_path):
|
| 453 |
man = json.load(open(man_path))
|
| 454 |
+
# Bound this report to the exact bytes it scanned: publish_mix re-computes the digest from the
|
| 455 |
+
# manifest it is about to ship, so an audit.json carried over from a different mix -- even one of
|
| 456 |
+
# the same total size -- is refused instead of certifying it.
|
| 457 |
+
manifest_digest = hashlib.sha256(json.dumps(
|
| 458 |
+
[[s["file"], s.get("tokens"), s.get("docs")] for s in man["shards"]] +
|
| 459 |
+
[["VAL:" + s["file"], s.get("tokens"), s.get("docs")] for s in man.get("val_shards", [])],
|
| 460 |
+
sort_keys=True).encode()).hexdigest()
|
| 461 |
+
seen_all = set() # one set across the whole mix, so duplicates are counted, not floored
|
| 462 |
for rec in man["shards"]:
|
| 463 |
p = os.path.join(a.root, rec["file"])
|
| 464 |
if not os.path.exists(p):
|
| 465 |
# A shard not on disk is unmeasured, not clean (E-017's shape).
|
| 466 |
missing.append(rec["file"])
|
| 467 |
continue
|
| 468 |
+
s = scan_docs(ref, refmask, doc_stream(BM, [p]), seen=seen_all)
|
| 469 |
docs += s["docs"]
|
| 470 |
mix_tokens += s["tokens"]
|
| 471 |
grams_seen += s["grams"]
|
|
|
|
| 475 |
dup_tokens += s["dup_tokens"]
|
| 476 |
for n, c in s["per_task_named"].items():
|
| 477 |
hits[n] = hits.get(n, 0) + c
|
| 478 |
+
# The held-out split is reported on, so it must be scanned too. A benchmark document sitting in
|
| 479 |
+
# validation would flatter every validation-PPL number this project publishes, and nothing else
|
| 480 |
+
# in the pipeline reads those bytes (review finding).
|
| 481 |
+
for rec in man.get("val_shards", []):
|
| 482 |
+
p = os.path.join(a.root, rec["file"])
|
| 483 |
+
if not os.path.exists(p):
|
| 484 |
+
missing.append(rec["file"])
|
| 485 |
+
continue
|
| 486 |
+
v = scan_docs(ref, refmask, doc_stream(BM, [p]), seen=seen_all)
|
| 487 |
+
for k2 in ("docs", "tokens", "grams", "hit_docs", "hits", "hit_docs_over_half"):
|
| 488 |
+
val[k2] += int(v.get(k2, 0))
|
| 489 |
+
total_overlap = sum(hits.values()) + val["hits"]
|
| 490 |
+
print(f" mix: {docs:,} docs, {grams_seen:,} grams, {sum(hits.values()):,} train hits in "
|
| 491 |
+
f"{hit_docs:,} docs ({hit_docs_over_half:,} majority-hit); val: {val['docs']:,} docs, "
|
| 492 |
+
f"{val['hits']:,} hits in {val['hit_docs']:,} docs", flush=True)
|
| 493 |
else:
|
| 494 |
total_overlap = 0
|
| 495 |
print(f"no manifest at {a.root}: merged mix not scanned", flush=True)
|
| 496 |
|
| 497 |
+
grams_all = grams_seen + val["grams"]
|
| 498 |
+
expected_fp = grams_all * int(np.unique(ref).size) / float(1 << 64)
|
| 499 |
noise_allow = max(10.0, 10.0 * expected_fp)
|
| 500 |
per_task_named = {t[0]: hits.get(t[0], 0) for t in REFS}
|
| 501 |
|
| 502 |
if a.write_filter:
|
| 503 |
os.makedirs(a.write_filter, exist_ok=True)
|
| 504 |
summary = {"k": K, "criterion": "any 13-gram of the document appears in a reference "
|
| 505 |
+
"train/validation/dev split",
|
| 506 |
+
"scanned_sources": sorted(masks), "sources": {}}
|
| 507 |
for key, (ord_sorted, n_docs) in masks.items():
|
| 508 |
m = np.zeros(n_docs, dtype=np.uint8)
|
| 509 |
if ord_sorted.size:
|
|
|
|
| 535 |
"mix_grams_audited": grams_seen,
|
| 536 |
"mix_documents_with_any_hit": hit_docs,
|
| 537 |
"mix_documents_majority_hit": hit_docs_over_half,
|
| 538 |
+
"val_documents_audited": val["docs"],
|
| 539 |
+
"val_tokens_audited": val["tokens"],
|
| 540 |
+
"val_documents_with_any_hit": val["hit_docs"],
|
| 541 |
+
"val_hits": val["hits"],
|
| 542 |
+
"mix_bytes_sha256": manifest_digest,
|
| 543 |
"overlap_by_task": per_task_named,
|
| 544 |
"overlap_total": total_overlap,
|
| 545 |
"by_source": by_source,
|
|
|
|
| 556 |
"mix_documents_majority_hit and source_hit_document_fraction to judge severity. "
|
| 557 |
"Duplicate counting is diagnostic only: de-duplication is out of scope (D-014), "
|
| 558 |
"contamination removal is not (§3.3)."),
|
| 559 |
+
"COVERED_ALL_SHARDS": bool(not missing and docs > 0 and val["docs"] > 0),
|
| 560 |
+
"FILTER_MASKS_WRITTEN": sorted(masks) if a.write_filter else None,
|
| 561 |
+
"MEASURED": bool(docs > 0 and grams_seen > 0 and val["docs"] > 0 and not errors
|
| 562 |
+
and len(tasks_covered) == 8 and manifest_digest
|
| 563 |
+
and (not a.write_filter or masks)),
|
| 564 |
}
|
| 565 |
report["AUDIT_PASSED"] = bool(report["MEASURED"] and total_overlap <= noise_allow)
|
| 566 |
txt = json.dumps(report, indent=1, default=str)
|