Cion-lab commited on
Commit
1f792cf
·
verified ·
1 Parent(s): 1b5ea42

Upload build/audit_contamination.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. build/audit_contamination.py +135 -44
build/audit_contamination.py CHANGED
@@ -47,13 +47,15 @@ REFS = [
47
  ("arc_easy", "allenai/ai2_arc", ["ARC-Easy"], ["train", "validation"]),
48
  ("arc_challenge", "allenai/ai2_arc", ["ARC-Challenge"], ["train", "validation"]),
49
  ("hellaswag", "Rowan/hellaswag", [None], ["train", "validation"]),
50
- ("piqa", "baber/piqa", [None, "plain_text"], ["train", "validation"]),
51
  ("winogrande", "allenai/winogrande", ["winogrande_xl"], ["train", "validation"]),
52
  ("mmlu", "cais/mmlu", ["all"], ["dev", "validation"]),
53
  ("truthfulqa", "truthfulqa/truthful_qa", ["multiple_choice", "generation"], ["validation"]),
54
  ("gsm8k", "openai/gsm8k", ["main"], ["train"]),
55
  ]
56
- TEXT_KEYS = ("question", "text", "choices", "story", "sentence", "prompt", "ctx", "input", "query")
 
 
57
  # A reference split that yields fewer rows than this did not load, it merely failed to raise: the first
58
  # run of this audit reported `piqa::default::train: 0 items -> 0 grams` with no exception, which a
59
  # coverage-blind verdict would have scored as "no contamination found in PIQA".
@@ -102,57 +104,141 @@ def flatten(v, out):
102
  flatten(x, out)
103
 
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  def build_reference(tk, max_items_per_split):
106
- """Returns (sorted uint64 array of every reference gram, parallel uint16 task-bitmask, per-split
107
- item counts, per-reference errors). Only counts and hashes are ever held or printed."""
108
- import datasets
109
 
 
 
 
 
110
  grams, masks = [], []
111
- counts, errors = {}, {}
 
 
 
 
 
112
  for ti, (task, repo, configs, splits) in enumerate(REFS):
113
  bit = np.uint16(1 << ti)
114
  for sp in splits:
115
- # One split, tried against each candidate config in order, until a loader yields rows. The
116
- # alternative -- trusting the first config that does not raise -- is what scored PIQA as
117
- # clean while reading 0 items from it.
118
- key, g, loaded = f"{task}::{repo}::{sp}", [], None
119
  for cfg in configs:
120
  ckey = f"{key}[{cfg or 'default'}]"
121
- try:
122
- ds = (datasets.load_dataset(repo, cfg, split=sp, streaming=True) if cfg
123
- else datasets.load_dataset(repo, split=sp, streaming=True))
124
- except Exception as e:
125
- errors[ckey] = f"{type(e).__name__}: {str(e)[:140]}"
126
- print(f" ref {ckey}: UNREADABLE {type(e).__name__}: {str(e)[:80]}", flush=True)
127
- continue
128
- n = 0
129
- for row in ds:
130
- if n >= max_items_per_split:
131
- break
132
- parts = []
133
- for k in TEXT_KEYS:
134
- if k in row:
135
- flatten(row[k], parts)
136
- txt = " ".join(parts)
137
- if not txt.strip():
138
  continue
139
- n += 1
140
- ids = np.asarray(tk.encode(txt, add_special_tokens=False).ids, dtype=np.uint16)
141
- h = window_hashes(ids)
142
- if h.size:
143
- g.append(h)
 
 
144
  counts[ckey] = n
145
- print(f" ref {ckey}: {n} items -> {sum(a.size for a in g):,} grams", flush=True)
146
- if n >= MIN_ITEMS_PER_SPLIT:
147
- loaded = ckey
 
 
148
  break
149
- errors[ckey] = f"only {n} rows (needs >= {MIN_ITEMS_PER_SPLIT}); trying next config"
150
- if loaded is None:
151
- errors[key] = f"no config of {repo} yielded >= {MIN_ITEMS_PER_SPLIT} rows for {sp}"
152
  continue
153
- per_task = [a for a in g]
154
- if per_task:
155
- arr = np.concatenate(per_task)
 
 
 
156
  grams.append(arr)
157
  masks.append(np.full(arr.shape, bit, dtype=np.uint16))
158
  if not grams:
@@ -160,7 +246,8 @@ def build_reference(tk, max_items_per_split):
160
  allg = np.concatenate(grams)
161
  allm = np.concatenate(masks)
162
  order = np.argsort(allg, kind="stable")
163
- return allg[order], allm[order], counts, errors
 
164
 
165
 
166
 
@@ -264,9 +351,10 @@ def main():
264
  tk_sha = hashlib.sha256(open(tjp, "rb").read()).hexdigest()
265
 
266
  want_meta = {"k": K, "max_items": a.max_items_per_split, "tokenizer_sha256": tk_sha,
 
267
  "refs": [[t[0], t[1], list(t[2]), list(t[3])] for t in REFS]}
268
  ref = refmask = None
269
- refcounts = errors = {}
270
  if a.ref_cache and os.path.exists(os.path.join(a.ref_cache, "refmeta.json")):
271
  try:
272
  if json.load(open(os.path.join(a.ref_cache, "refmeta.json"))) == want_meta:
@@ -274,6 +362,7 @@ def main():
274
  refmask = np.load(os.path.join(a.ref_cache, "refmask.npy"))
275
  refcounts = json.load(open(os.path.join(a.ref_cache, "refcounts.json")))
276
  errors = json.load(open(os.path.join(a.ref_cache, "errors.json")))
 
277
  print(f"reference: loaded from cache, {ref.size:,} grams", flush=True)
278
  except Exception as e:
279
  print(f"reference cache unusable ({type(e).__name__}: {str(e)[:120]}); rebuilding",
@@ -281,7 +370,7 @@ def main():
281
  ref = None
282
  if ref is None:
283
  print("building reference gram table (only counts ever leave this process)", flush=True)
284
- ref, refmask, refcounts, errors = build_reference(tk, a.max_items_per_split)
285
  if a.ref_cache:
286
  os.makedirs(a.ref_cache, exist_ok=True)
287
  np.save(os.path.join(a.ref_cache, "ref.npy"), ref)
@@ -289,6 +378,7 @@ def main():
289
  json.dump(want_meta, open(os.path.join(a.ref_cache, "refmeta.json"), "w"))
290
  json.dump(refcounts, open(os.path.join(a.ref_cache, "refcounts.json"), "w"))
291
  json.dump(errors, open(os.path.join(a.ref_cache, "errors.json"), "w"))
 
292
  print(f"reference cached under {a.ref_cache}", flush=True)
293
  tasks_covered = sorted({k.split("::")[0] for k, v in refcounts.items()
294
  if v >= MIN_ITEMS_PER_SPLIT})
@@ -387,6 +477,7 @@ def main():
387
  "k": K,
388
  "fingerprint": "multiplicative rolling hash mod 2^64, P=0x100000001B3",
389
  "reference_items_by_split": refcounts,
 
390
  "reference_loader_problems": errors,
391
  "reference_grams_distinct": int(np.unique(ref).size),
392
  "reference_grams_total": int(ref.size),
 
47
  ("arc_easy", "allenai/ai2_arc", ["ARC-Easy"], ["train", "validation"]),
48
  ("arc_challenge", "allenai/ai2_arc", ["ARC-Challenge"], ["train", "validation"]),
49
  ("hellaswag", "Rowan/hellaswag", [None], ["train", "validation"]),
50
+ ("piqa", "baber/piqa", [None], ["train", "validation"]),
51
  ("winogrande", "allenai/winogrande", ["winogrande_xl"], ["train", "validation"]),
52
  ("mmlu", "cais/mmlu", ["all"], ["dev", "validation"]),
53
  ("truthfulqa", "truthfulqa/truthful_qa", ["multiple_choice", "generation"], ["validation"]),
54
  ("gsm8k", "openai/gsm8k", ["main"], ["train"]),
55
  ]
56
+ TEXT_KEYS = ("question", "text", "choices", "story", "sentence", "prompt", "ctx", "input", "query",
57
+ "answer", "solution", "reasoning", "explanation", "goal", "sol1", "sol2", "abstractions",
58
+ "nonabstractions", "mc1_targets", "mc2_targets")
59
  # A reference split that yields fewer rows than this did not load, it merely failed to raise: the first
60
  # run of this audit reported `piqa::default::train: 0 items -> 0 grams` with no exception, which a
61
  # coverage-blind verdict would have scored as "no contamination found in PIQA".
 
104
  flatten(x, out)
105
 
106
 
107
+ SERVER = "https://datasets-server.huggingface.co"
108
+ _SIZE_CACHE = {}
109
+
110
+
111
+ def server_sizes(repo, cfg):
112
+ """Row counts the dataset server reports for each split of one config, or {} if it cannot be asked.
113
+
114
+ This exists because a streaming reader can stop early without raising, and because a loader that yields
115
+ 0 rows of a 16,113-row split (exactly what `baber/piqa` did under the Kaggle image's `datasets`) is
116
+ indistinguishable from a clean result unless the expected count is checked. Never trust the count from
117
+ memory: the first version of this file expected 14,042 MMLU validation rows, and 14,042 is MMLU's
118
+ *test* split -- the one §3.3 forbids reading. `validation` is 1,531.
119
+ """
120
+ import urllib.parse
121
+ import urllib.request
122
+ ck = (repo, cfg)
123
+ if ck in _SIZE_CACHE:
124
+ return _SIZE_CACHE[ck]
125
+ q = {"dataset": repo}
126
+ if cfg:
127
+ q["config"] = cfg
128
+ out = {}
129
+ try:
130
+ with urllib.request.urlopen(SERVER + "/size?" + urllib.parse.urlencode(q), timeout=90) as r:
131
+ d = json.load(r)
132
+ out = {s["split"]: int(s.get("num_rows", 0)) for s in d.get("size", {}).get("splits", [])}
133
+ except Exception as e:
134
+ print(f" sizes API {repo}/{cfg or ''}: {type(e).__name__}", flush=True)
135
+ _SIZE_CACHE[ck] = out
136
+ return out
137
+
138
+
139
+ def api_rows(repo, cfg, sp, cap):
140
+ """Page the public rows endpoint (100 per request, full=true so no cell is silently truncated)."""
141
+ import urllib.parse
142
+ import urllib.request
143
+ off = 0
144
+ while off < cap:
145
+ q = {"dataset": repo, "split": sp, "offset": off, "limit": 100, "full": "true"}
146
+ if cfg:
147
+ q["config"] = cfg
148
+ try:
149
+ with urllib.request.urlopen(SERVER + "/rows?" + urllib.parse.urlencode(q),
150
+ timeout=120) as r:
151
+ d = json.load(r)
152
+ except Exception as e:
153
+ print(f" rows API {repo}/{cfg or ''}/{sp} @{off}: {type(e).__name__}", flush=True)
154
+ return
155
+ batch = d.get("rows") or []
156
+ if not batch:
157
+ return
158
+ for item in batch:
159
+ yield item.get("row", item)
160
+ off += len(batch)
161
+
162
+
163
+ def ingest(tk, rows, cap):
164
+ """Encode each row's text fields into gram windows. Returns (items kept, list of gram arrays)."""
165
+ g, n = [], 0
166
+ for row in rows:
167
+ if n >= cap:
168
+ break
169
+ parts = []
170
+ for k in TEXT_KEYS:
171
+ if k in row:
172
+ flatten(row[k], parts)
173
+ txt = " ".join(parts)
174
+ if not txt.strip():
175
+ continue
176
+ n += 1
177
+ ids = np.asarray(tk.encode(txt, add_special_tokens=False).ids, dtype=np.uint16)
178
+ h = window_hashes(ids)
179
+ if h.size:
180
+ g.append(h)
181
+ return n, g
182
+
183
+
184
  def build_reference(tk, max_items_per_split):
185
+ """Returns (sorted uint64 reference grams, parallel uint16 task bitmask, per-split item counts,
186
+ errors, per-split expected row counts). Only counts and hashes are ever held or printed.
 
187
 
188
+ A split is accepted only if the rows actually read reach 90 % of what the dataset server says the
189
+ split holds; short reads are retried through the HTTP rows endpoint and, if still short, recorded as
190
+ loader problems -- which fail MEASURED rather than scoring as "no contamination found".
191
+ """
192
  grams, masks = [], []
193
+ counts, errors, srv = {}, {}, {}
194
+ try:
195
+ import datasets
196
+ except Exception as e:
197
+ datasets = None
198
+ errors["import datasets"] = f"{type(e).__name__}: {str(e)[:120]}"
199
  for ti, (task, repo, configs, splits) in enumerate(REFS):
200
  bit = np.uint16(1 << ti)
201
  for sp in splits:
202
+ key = f"{task}::{repo}::{sp}"
203
+ best = None
 
 
204
  for cfg in configs:
205
  ckey = f"{key}[{cfg or 'default'}]"
206
+ want = server_sizes(repo, cfg).get(sp)
207
+ srv[key] = want
208
+ n, g = 0, []
209
+ if datasets is not None:
210
+ try:
211
+ ds = (datasets.load_dataset(repo, cfg, split=sp, streaming=True) if cfg
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] = f"{type(e).__name__}: {str(e)[:140]}"
216
+ print(f" ref {ckey}: UNREADABLE {type(e).__name__}: {str(e)[:80]}", flush=True)
 
 
 
 
 
 
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",
220
+ flush=True)
221
+ n2, g2 = ingest(tk, api_rows(repo, cfg, sp, max_items_per_split),
222
+ max_items_per_split)
223
+ if n2 > n:
224
+ n, g = n2, g2
225
  counts[ckey] = n
226
+ print(f" ref {ckey}: {n} items of {want} -> {sum(a.size for a in g):,} grams",
227
+ flush=True)
228
+ if best is None or n > best[1]:
229
+ best = (cfg, n, g, want)
230
+ if n >= MIN_ITEMS_PER_SPLIT and (not want or n >= int(0.9 * want)):
231
  break
232
+ if best is None or best[1] < MIN_ITEMS_PER_SPLIT:
233
+ errors[key] = (f"no config of {repo} yielded >= {MIN_ITEMS_PER_SPLIT} rows for {sp} "
234
+ f"(best {(best[1] if best else 0)} of {srv.get(key)})")
235
  continue
236
+ cfg, n, g, want = best
237
+ if want and n < int(0.9 * want):
238
+ errors[key] = f"covered {n} of {want} rows in {sp} -- under-read, not calling it clean"
239
+ continue
240
+ if g:
241
+ arr = np.concatenate(g)
242
  grams.append(arr)
243
  masks.append(np.full(arr.shape, bit, dtype=np.uint16))
244
  if not grams:
 
246
  allg = np.concatenate(grams)
247
  allm = np.concatenate(masks)
248
  order = np.argsort(allg, kind="stable")
249
+ return allg[order], allm[order], counts, errors, srv
250
+
251
 
252
 
253
 
 
351
  tk_sha = hashlib.sha256(open(tjp, "rb").read()).hexdigest()
352
 
353
  want_meta = {"k": K, "max_items": a.max_items_per_split, "tokenizer_sha256": tk_sha,
354
+ "text_keys": list(TEXT_KEYS),
355
  "refs": [[t[0], t[1], list(t[2]), list(t[3])] for t in REFS]}
356
  ref = refmask = None
357
+ refcounts = errors = srv = {}
358
  if a.ref_cache and os.path.exists(os.path.join(a.ref_cache, "refmeta.json")):
359
  try:
360
  if json.load(open(os.path.join(a.ref_cache, "refmeta.json"))) == want_meta:
 
362
  refmask = np.load(os.path.join(a.ref_cache, "refmask.npy"))
363
  refcounts = json.load(open(os.path.join(a.ref_cache, "refcounts.json")))
364
  errors = json.load(open(os.path.join(a.ref_cache, "errors.json")))
365
+ srv = json.load(open(os.path.join(a.ref_cache, "srv.json")))
366
  print(f"reference: loaded from cache, {ref.size:,} grams", flush=True)
367
  except Exception as e:
368
  print(f"reference cache unusable ({type(e).__name__}: {str(e)[:120]}); rebuilding",
 
370
  ref = None
371
  if ref is None:
372
  print("building reference gram table (only counts ever leave this process)", flush=True)
373
+ ref, refmask, refcounts, errors, srv = build_reference(tk, a.max_items_per_split)
374
  if a.ref_cache:
375
  os.makedirs(a.ref_cache, exist_ok=True)
376
  np.save(os.path.join(a.ref_cache, "ref.npy"), ref)
 
378
  json.dump(want_meta, open(os.path.join(a.ref_cache, "refmeta.json"), "w"))
379
  json.dump(refcounts, open(os.path.join(a.ref_cache, "refcounts.json"), "w"))
380
  json.dump(errors, open(os.path.join(a.ref_cache, "errors.json"), "w"))
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})
 
477
  "k": K,
478
  "fingerprint": "multiplicative rolling hash mod 2^64, P=0x100000001B3",
479
  "reference_items_by_split": refcounts,
480
+ "reference_rows_expected_by_split": srv,
481
  "reference_loader_problems": errors,
482
  "reference_grams_distinct": int(np.unique(ref).size),
483
  "reference_grams_total": int(ref.size),