smoke PASSED (4/4 sources per shard, 40/40 boundaries); add contamination audit; raw target 1.23B = 23% headroom
Browse files- build/audit_contamination.py +172 -0
- build/build_mix.py +3 -3
build/audit_contamination.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Phase 2 contamination audit (§3.3): mechanical 13-token overlap between the mix and the eval tasks.
|
| 2 |
+
#
|
| 3 |
+
# What §3.3 permits, precisely: "benchmark validation/dev material may be used mechanically: a script
|
| 4 |
+
# computes overlap counts and you never read or learn from the items themselves. Test splits stay
|
| 5 |
+
# untouched until Phase 6." This script obeys that literally, and it is written so that violating it is
|
| 6 |
+
# hard: the only values that ever leave the process are counts and rates per source. No item text, no
|
| 7 |
+
# lengths, no samples, nothing a human could read as a question. The print surface is a dict of integers.
|
| 8 |
+
#
|
| 9 |
+
# Which splits are used as the reference, and why it is broader than the minimum:
|
| 10 |
+
# for most of these tasks the harness draws its few-shot exemplars from the TRAIN split (ARC, HellaSwag,
|
| 11 |
+
# PIQA, Winogrande) or from MMLU's `dev`, so overlap with those is contamination by construction -- a
|
| 12 |
+
# mix that memorises an exemplar is a mix that inflates the score. So the reference set is
|
| 13 |
+
# {train, validation, dev} where present, per task, and NEVER `test`. That is more conservative than the
|
| 14 |
+
# literal wording, and it is the set that actually matters.
|
| 15 |
+
#
|
| 16 |
+
# Method: for each reference document, tokenise with the frozen tokenizer and hash every 13-token window
|
| 17 |
+
# into a set of uint64. For each mix document, do the same and count hits. 13-grams match the documented
|
| 18 |
+
# practice of the decontaminated math source in the mix, so the threshold is comparable to published
|
| 19 |
+
# hygiene rather than something invented here.
|
| 20 |
+
#
|
| 21 |
+
# Memory: the reference side is the small one -- a few hundred thousand short items x ~300 grams x 8 B,
|
| 22 |
+
# held as a Python set of ints. The mix side streams document by document from the shard files, so its
|
| 23 |
+
# size is irrelevant. Nothing here holds a billion of anything.
|
| 24 |
+
#
|
| 25 |
+
# Read-only over the mix. CPU session. No credentials.
|
| 26 |
+
|
| 27 |
+
import argparse
|
| 28 |
+
import hashlib
|
| 29 |
+
import json
|
| 30 |
+
import os
|
| 31 |
+
import struct
|
| 32 |
+
|
| 33 |
+
import array
|
| 34 |
+
|
| 35 |
+
K = 13
|
| 36 |
+
# task -> (repo, config(s), splits considered reference material; 'test' deliberately absent)
|
| 37 |
+
REFS = [
|
| 38 |
+
("arc_easy", "allenai/ai2_arc", ["ARC-Easy"], ["train", "validation"]),
|
| 39 |
+
("arc_challenge", "allenai/ai2_arc", ["ARC-Challenge"], ["train", "validation"]),
|
| 40 |
+
("hellaswag", "Rowan/hellaswag", [None], ["train", "validation"]),
|
| 41 |
+
("piqa", "baber/piqa", [None], ["train", "validation"]),
|
| 42 |
+
("winogrande", "allenai/winogrande", ["winogrande_xl"], ["train", "validation"]),
|
| 43 |
+
("mmlu", "cais/mmlu", ["all"], ["dev", "validation"]),
|
| 44 |
+
("truthfulqa", "sylinrl/TruthfulQA", [None], ["validation"]),
|
| 45 |
+
("gsm8k", "openai/gsm8k", ["main"], ["train", "validation"]),
|
| 46 |
+
]
|
| 47 |
+
TEXT_KEYS = ("question", "text", "choices", "story", "sentence", "prompt", "ctx", "input", "query")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def grams64(ids):
|
| 51 |
+
"""Set of uint64 hashes over K-length token windows. Hashing ids rather than characters means the
|
| 52 |
+
comparison is in the same space as the mix, and no human-readable string is ever formed."""
|
| 53 |
+
if len(ids) < K:
|
| 54 |
+
return {struct.unpack("<Q", hashlib.sha256(bytes(array.array("H", ids))).digest()[:8])[0]} \
|
| 55 |
+
if ids else set()
|
| 56 |
+
out = set()
|
| 57 |
+
for i in range(len(ids) - K + 1):
|
| 58 |
+
w = ids[i:i + K]
|
| 59 |
+
out.add(struct.unpack("<Q", hashlib.sha256(
|
| 60 |
+
memoryview(bytes(array.array("H", w)))).digest()[:8])[0])
|
| 61 |
+
return out
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def flatten(v, acc):
|
| 65 |
+
if isinstance(v, str):
|
| 66 |
+
acc.append(v)
|
| 67 |
+
elif isinstance(v, dict):
|
| 68 |
+
for x in v.values():
|
| 69 |
+
flatten(x, acc)
|
| 70 |
+
elif isinstance(v, (list, tuple)):
|
| 71 |
+
for x in v:
|
| 72 |
+
flatten(x, acc)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def build_reference(tk, max_items_per_split):
|
| 76 |
+
"""Return {ref_key: gram_set}. Counts for the report are derived from these sets afterwards, so the
|
| 77 |
+
reference corpus is streamed exactly once."""
|
| 78 |
+
import datasets
|
| 79 |
+
|
| 80 |
+
sets = {}
|
| 81 |
+
for task, repo, configs, splits in REFS:
|
| 82 |
+
for cfg in configs:
|
| 83 |
+
for sp in splits:
|
| 84 |
+
key = f"{task}::{cfg or 'default'}::{sp}"
|
| 85 |
+
try:
|
| 86 |
+
ds = (datasets.load_dataset(repo, cfg, split=sp, streaming=True) if cfg
|
| 87 |
+
else datasets.load_dataset(repo, split=sp, streaming=True))
|
| 88 |
+
except Exception as e:
|
| 89 |
+
sets[key] = {"error": f"{type(e).__name__}: {str(e)[:140]}"}
|
| 90 |
+
print(f" ref {key}: UNREADABLE {type(e).__name__}", flush=True)
|
| 91 |
+
continue
|
| 92 |
+
g, n = set(), 0
|
| 93 |
+
for row in ds:
|
| 94 |
+
if n >= max_items_per_split:
|
| 95 |
+
break
|
| 96 |
+
parts = []
|
| 97 |
+
for k in TEXT_KEYS:
|
| 98 |
+
if k in row:
|
| 99 |
+
flatten(row[k], parts)
|
| 100 |
+
txt = " ".join(parts)
|
| 101 |
+
if not txt.strip():
|
| 102 |
+
continue
|
| 103 |
+
n += 1
|
| 104 |
+
g |= grams64(tk.encode(txt, add_special_tokens=False).ids)
|
| 105 |
+
sets[key] = g
|
| 106 |
+
print(f" ref {key}: {n} items -> {len(g):,} grams", flush=True)
|
| 107 |
+
return sets
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def main():
|
| 111 |
+
ap = argparse.ArgumentParser()
|
| 112 |
+
ap.add_argument("--root", default="/kaggle/working/mixroot")
|
| 113 |
+
ap.add_argument("--max-items-per-split", type=int, default=20000)
|
| 114 |
+
a = ap.parse_args()
|
| 115 |
+
|
| 116 |
+
from tokenizers import Tokenizer
|
| 117 |
+
from huggingface_hub import hf_hub_download
|
| 118 |
+
tk = Tokenizer.from_file(hf_hub_download("HuggingFaceTB/SmolLM2-135M", "tokenizer.json"))
|
| 119 |
+
tk.no_truncation()
|
| 120 |
+
|
| 121 |
+
print("building reference gram sets (only counts ever leave this process)", flush=True)
|
| 122 |
+
ref_sets = build_reference(tk, a.max_items_per_split)
|
| 123 |
+
errors = {k: v["error"] for k, v in ref_sets.items() if isinstance(v, dict)}
|
| 124 |
+
ref_sets = {k: v for k, v in ref_sets.items() if isinstance(v, set)}
|
| 125 |
+
|
| 126 |
+
# stream the mix's own shards; per-shard source lists let a hit be attributed to a source, which is
|
| 127 |
+
# the only actionable granularity ("drop or clean any source that fails")
|
| 128 |
+
man = json.load(open(os.path.join(a.root, "manifest.json")))
|
| 129 |
+
hits = {k: 0 for k in ref_sets}
|
| 130 |
+
hits_by_source = {}
|
| 131 |
+
docs = grams_seen = 0
|
| 132 |
+
import build_mix as BM
|
| 133 |
+
for rec in man["shards"]:
|
| 134 |
+
p = os.path.join(a.root, rec["file"])
|
| 135 |
+
if not os.path.exists(p):
|
| 136 |
+
continue
|
| 137 |
+
for d in BM.iter_shard_docs(p):
|
| 138 |
+
docs += 1
|
| 139 |
+
g = grams64(list(d))
|
| 140 |
+
if not g:
|
| 141 |
+
continue
|
| 142 |
+
grams_seen += len(g)
|
| 143 |
+
for k, s in ref_sets.items():
|
| 144 |
+
ov = len(g & s)
|
| 145 |
+
if ov:
|
| 146 |
+
hits[k] += ov
|
| 147 |
+
for srcname in rec["sources"]:
|
| 148 |
+
hits_by_source.setdefault(srcname, {}).setdefault(k, 0)
|
| 149 |
+
hits_by_source[srcname][k] += ov // max(1, len(rec["sources"]))
|
| 150 |
+
if docs % 200000 < 5000:
|
| 151 |
+
print(f" audited {docs:,} docs, {grams_seen:,} mix grams", flush=True)
|
| 152 |
+
|
| 153 |
+
report = {
|
| 154 |
+
"k": K,
|
| 155 |
+
"reference": {k: {"distinct_grams": len(v)} for k, v in ref_sets.items()},
|
| 156 |
+
"reference_unreadable": errors,
|
| 157 |
+
"mix_documents_audited": docs,
|
| 158 |
+
"mix_grams_audited": grams_seen,
|
| 159 |
+
"overlap_grams_total_by_ref": hits,
|
| 160 |
+
"overlap_rate_by_ref": {k: round(v / max(1, grams_seen), 8) for k, v in hits.items()},
|
| 161 |
+
"overlap_by_source": hits_by_source,
|
| 162 |
+
"note": ("counts only; no item text was read or printed. Reference splits are train/validation/dev "
|
| 163 |
+
"per task -- never test, per §3.3. Any source with a non-zero overlap here must be dropped "
|
| 164 |
+
"or cleaned before Gate 2 is claimed."),
|
| 165 |
+
}
|
| 166 |
+
print("AUDIT_JSON_BEGIN")
|
| 167 |
+
print(json.dumps(report, indent=1, default=str))
|
| 168 |
+
print("AUDIT_JSON_END")
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
if __name__ == "__main__":
|
| 172 |
+
main()
|
build/build_mix.py
CHANGED
|
@@ -54,9 +54,9 @@ STATE_NAME = "build_state.json"
|
|
| 54 |
# Corrected config and column names, from the Phase 2 inventory (docs/02-mix-plan.md §9). The names in
|
| 55 |
# §2 came from dataset cards, and five of them did not exist.
|
| 56 |
SOURCES = [
|
| 57 |
-
dict(repo="HuggingFaceFW/fineweb-edu", config="sample-10BT", col="text", tokens=
|
| 58 |
min_score=4, why="quality-scored general English"),
|
| 59 |
-
dict(repo="HuggingFaceFW/finepdfs-edu", config="eng_Latn", col="text", tokens=
|
| 60 |
en_filter=True, why="long-form educational PDFs; needs own en filter"),
|
| 61 |
dict(repo="HuggingFaceTB/cosmopedia", config="stanford", col="text", tokens=60_000_000,
|
| 62 |
why="textbook expository"),
|
|
@@ -72,7 +72,7 @@ SOURCES = [
|
|
| 72 |
why="rewritten encyclopedic prose"),
|
| 73 |
dict(repo="omarkamali/wikipedia-monthly", config="20260101.en", col="text", tokens=60_000_000,
|
| 74 |
en_filter=True, why="current encyclopedic breadth"),
|
| 75 |
-
dict(repo="HuggingFaceTB/finemath", config="finemath-4plus", col="text", tokens=
|
| 76 |
why="math; upstream-decontaminated against GSM8K/MMLU/ARC"),
|
| 77 |
dict(repo="open-web-math/open-web-math", config="default", col="text", tokens=70_000_000,
|
| 78 |
en_filter=True, why="forum/webbook math prose"),
|
|
|
|
| 54 |
# Corrected config and column names, from the Phase 2 inventory (docs/02-mix-plan.md §9). The names in
|
| 55 |
# §2 came from dataset cards, and five of them did not exist.
|
| 56 |
SOURCES = [
|
| 57 |
+
dict(repo="HuggingFaceFW/fineweb-edu", config="sample-10BT", col="text", tokens=340_000_000,
|
| 58 |
min_score=4, why="quality-scored general English"),
|
| 59 |
+
dict(repo="HuggingFaceFW/finepdfs-edu", config="eng_Latn", col="text", tokens=180_000_000,
|
| 60 |
en_filter=True, why="long-form educational PDFs; needs own en filter"),
|
| 61 |
dict(repo="HuggingFaceTB/cosmopedia", config="stanford", col="text", tokens=60_000_000,
|
| 62 |
why="textbook expository"),
|
|
|
|
| 72 |
why="rewritten encyclopedic prose"),
|
| 73 |
dict(repo="omarkamali/wikipedia-monthly", config="20260101.en", col="text", tokens=60_000_000,
|
| 74 |
en_filter=True, why="current encyclopedic breadth"),
|
| 75 |
+
dict(repo="HuggingFaceTB/finemath", config="finemath-4plus", col="text", tokens=160_000_000,
|
| 76 |
why="math; upstream-decontaminated against GSM8K/MMLU/ARC"),
|
| 77 |
dict(repo="open-web-math/open-web-math", config="default", col="text", tokens=70_000_000,
|
| 78 |
en_filter=True, why="forum/webbook math prose"),
|