Cion-lab commited on
Commit
18d6af8
·
verified ·
1 Parent(s): 20765b9

smoke PASSED (4/4 sources per shard, 40/40 boundaries); add contamination audit; raw target 1.23B = 23% headroom

Browse files
Files changed (2) hide show
  1. build/audit_contamination.py +172 -0
  2. 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=300_000_000,
58
  min_score=4, why="quality-scored general English"),
59
- dict(repo="HuggingFaceFW/finepdfs-edu", config="eng_Latn", col="text", tokens=150_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,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=130_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"),
 
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"),