Upload build/audit_contamination.py with huggingface_hub
Browse files- build/audit_contamination.py +230 -70
build/audit_contamination.py
CHANGED
|
@@ -39,18 +39,26 @@ P = np.uint64(0x100000001B3) # FNV-1a prime, odd, so invertible mod 2^6
|
|
| 39 |
PINV = np.uint64(pow(int(P), -1, 1 << 64))
|
| 40 |
MODMASK = np.uint64((1 << 64) - 1)
|
| 41 |
|
| 42 |
-
# task -> (repo, config(s), splits treated as reference material; 'test' deliberately
|
|
|
|
|
|
|
|
|
|
| 43 |
REFS = [
|
| 44 |
("arc_easy", "allenai/ai2_arc", ["ARC-Easy"], ["train", "validation"]),
|
| 45 |
("arc_challenge", "allenai/ai2_arc", ["ARC-Challenge"], ["train", "validation"]),
|
| 46 |
("hellaswag", "Rowan/hellaswag", [None], ["train", "validation"]),
|
| 47 |
-
("piqa", "baber/piqa", [None], ["train", "validation"]),
|
| 48 |
("winogrande", "allenai/winogrande", ["winogrande_xl"], ["train", "validation"]),
|
| 49 |
("mmlu", "cais/mmlu", ["all"], ["dev", "validation"]),
|
| 50 |
-
("truthfulqa", "
|
| 51 |
-
("gsm8k", "openai/gsm8k", ["main"], ["train"
|
| 52 |
]
|
| 53 |
TEXT_KEYS = ("question", "text", "choices", "story", "sentence", "prompt", "ctx", "input", "query")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
|
| 56 |
def window_hashes(ids, k=K):
|
|
@@ -95,7 +103,7 @@ def flatten(v, out):
|
|
| 95 |
|
| 96 |
|
| 97 |
def build_reference(tk, max_items_per_split):
|
| 98 |
-
"""Returns (sorted uint64 array of every reference gram, parallel uint16 task-bitmask, per-
|
| 99 |
item counts, per-reference errors). Only counts and hashes are ever held or printed."""
|
| 100 |
import datasets
|
| 101 |
|
|
@@ -103,17 +111,21 @@ def build_reference(tk, max_items_per_split):
|
|
| 103 |
counts, errors = {}, {}
|
| 104 |
for ti, (task, repo, configs, splits) in enumerate(REFS):
|
| 105 |
bit = np.uint16(1 << ti)
|
| 106 |
-
for
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
try:
|
| 110 |
ds = (datasets.load_dataset(repo, cfg, split=sp, streaming=True) if cfg
|
| 111 |
else datasets.load_dataset(repo, split=sp, streaming=True))
|
| 112 |
except Exception as e:
|
| 113 |
-
errors[
|
| 114 |
-
print(f" ref {
|
| 115 |
continue
|
| 116 |
-
|
| 117 |
for row in ds:
|
| 118 |
if n >= max_items_per_split:
|
| 119 |
break
|
|
@@ -129,12 +141,20 @@ def build_reference(tk, max_items_per_split):
|
|
| 129 |
h = window_hashes(ids)
|
| 130 |
if h.size:
|
| 131 |
g.append(h)
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
if not grams:
|
| 139 |
raise SystemExit("no reference grams built at all -- the audit would be a pass over nothing")
|
| 140 |
allg = np.concatenate(grams)
|
|
@@ -143,10 +163,89 @@ def build_reference(tk, max_items_per_split):
|
|
| 143 |
return allg[order], allm[order], counts, errors
|
| 144 |
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
def main():
|
| 147 |
ap = argparse.ArgumentParser()
|
| 148 |
ap.add_argument("--root", default="/kaggle/working/mixroot")
|
| 149 |
-
ap.add_argument("--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
ap.add_argument("--out", default="", help="write the report JSON here (publish_mix expects "
|
| 151 |
"mixroot/audit.json)")
|
| 152 |
a = ap.parse_args()
|
|
@@ -158,77 +257,138 @@ def main():
|
|
| 158 |
|
| 159 |
print("building reference gram table (only counts ever leave this process)", flush=True)
|
| 160 |
ref, refmask, refcounts, errors = build_reference(tk, a.max_items_per_split)
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
| 163 |
|
| 164 |
-
man = json.load(open(os.path.join(a.root, "manifest.json")))
|
| 165 |
import build_mix as BM
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
hits = {}
|
| 168 |
-
|
| 169 |
-
|
| 170 |
missing = []
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
docs += 1
|
| 179 |
-
ids = np.frombuffer(bytes(d), dtype=np.uint16)
|
| 180 |
-
g = window_hashes(ids)
|
| 181 |
-
if g.size == 0:
|
| 182 |
continue
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
report = {
|
| 204 |
"k": K,
|
| 205 |
"fingerprint": "multiplicative rolling hash mod 2^64, P=0x100000001B3",
|
| 206 |
"reference_items_by_split": refcounts,
|
| 207 |
-
"
|
| 208 |
"reference_grams_distinct": int(np.unique(ref).size),
|
| 209 |
"reference_grams_total": int(ref.size),
|
| 210 |
-
"
|
| 211 |
"shards_missing": missing,
|
| 212 |
"mix_documents_audited": docs,
|
|
|
|
| 213 |
"mix_grams_audited": grams_seen,
|
| 214 |
-
"
|
| 215 |
-
"
|
| 216 |
-
"
|
| 217 |
"overlap_total": total_overlap,
|
| 218 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
"note": ("counts only; no item text read or printed. Reference splits are train/validation/dev "
|
| 220 |
-
"per task -- never test, per §3.3.
|
| 221 |
-
"
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
}
|
|
|
|
| 228 |
txt = json.dumps(report, indent=1, default=str)
|
| 229 |
-
print(f"VERDICT AUDIT_PASSED={report['AUDIT_PASSED']}
|
| 230 |
-
f"
|
| 231 |
-
f"
|
|
|
|
| 232 |
print("AUDIT_JSON_BEGIN")
|
| 233 |
print(txt)
|
| 234 |
print("AUDIT_JSON_END")
|
|
|
|
| 39 |
PINV = np.uint64(pow(int(P), -1, 1 << 64))
|
| 40 |
MODMASK = np.uint64((1 << 64) - 1)
|
| 41 |
|
| 42 |
+
# task -> (repo, config(s) to try in order, splits treated as reference material; 'test' deliberately
|
| 43 |
+
# absent). Split names were read off the dataset server on 2026-09-19 rather than assumed: gsm8k has
|
| 44 |
+
# train/test only, truthfulqa's repo is `truthfulqa/truthful_qa` and exposes `validation` only, and
|
| 45 |
+
# `sylinrl/TruthfulQA` -- the name the first version of this file used -- no longer exists.
|
| 46 |
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".
|
| 60 |
+
MIN_ITEMS_PER_SPLIT = 200
|
| 61 |
+
|
| 62 |
|
| 63 |
|
| 64 |
def window_hashes(ids, k=K):
|
|
|
|
| 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 |
|
|
|
|
| 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
|
|
|
|
| 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:
|
| 159 |
raise SystemExit("no reference grams built at all -- the audit would be a pass over nothing")
|
| 160 |
allg = np.concatenate(grams)
|
|
|
|
| 163 |
return allg[order], allm[order], counts, errors
|
| 164 |
|
| 165 |
|
| 166 |
+
|
| 167 |
+
def gram_lookup(g, ref, refmask):
|
| 168 |
+
"""(number of hits, the task bitmask of the matched reference grams) for one document.
|
| 169 |
+
|
| 170 |
+
numpy only, because this runs ~1.1 M times. `searchsorted` into the sorted reference table plus the
|
| 171 |
+
parallel bitmask is what makes per-task attribution possible without holding eight sets.
|
| 172 |
+
"""
|
| 173 |
+
if g.size == 0:
|
| 174 |
+
return 0, None
|
| 175 |
+
pos = np.clip(np.searchsorted(ref, g), 0, ref.size - 1)
|
| 176 |
+
eq = ref[pos] == g
|
| 177 |
+
if not eq.any():
|
| 178 |
+
return 0, None
|
| 179 |
+
return int(eq.sum()), refmask[pos[eq]]
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def per_task_counts(bits):
|
| 183 |
+
return [int((bits & np.uint16(1 << ti)).astype(np.uint16).sum()) for ti in range(len(REFS))]
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def scan_docs(ref, refmask, docs):
|
| 187 |
+
"""Fold an iterator of (ordinal_within_source, raw_bytes) into counts.
|
| 188 |
+
|
| 189 |
+
The ordinal is the only identity a document ever gets here: a filter pass can drop it by index later
|
| 190 |
+
without anything in this process knowing what it said. `hit_docs_over_half` is the column that decides
|
| 191 |
+
interpretation -- contamination by verbatim inclusion shows up as documents whose grams are *mostly*
|
| 192 |
+
matched, while one shared boilerplate sentence shows up as a single gram in a handful of documents.
|
| 193 |
+
"""
|
| 194 |
+
import time
|
| 195 |
+
import hashlib
|
| 196 |
+
t0 = time.time()
|
| 197 |
+
s = {"docs": 0, "tokens": 0, "grams": 0, "hit_docs": 0, "hits": 0, "hit_docs_over_half": 0,
|
| 198 |
+
"hit_tokens": 0, "dup_docs": 0, "dup_tokens": 0, "per_task": [0] * len(REFS), "ordinals": []}
|
| 199 |
+
seen = set()
|
| 200 |
+
names = [t[0] for t in REFS]
|
| 201 |
+
for idx, raw in docs:
|
| 202 |
+
b = bytes(raw)
|
| 203 |
+
ids = np.frombuffer(b, dtype=np.uint16)
|
| 204 |
+
s["docs"] += 1
|
| 205 |
+
s["tokens"] += int(ids.size)
|
| 206 |
+
dg = hashlib.blake2b(b, digest_size=8).digest()
|
| 207 |
+
if dg in seen: # counted, never removed: de-duplication is out of scope (D-014)
|
| 208 |
+
s["dup_docs"] += 1
|
| 209 |
+
s["dup_tokens"] += int(ids.size)
|
| 210 |
+
else:
|
| 211 |
+
seen.add(dg)
|
| 212 |
+
g = window_hashes(ids)
|
| 213 |
+
s["grams"] += int(g.size)
|
| 214 |
+
h, bits = gram_lookup(g, ref, refmask)
|
| 215 |
+
if h:
|
| 216 |
+
s["hit_docs"] += 1
|
| 217 |
+
s["hits"] += h
|
| 218 |
+
s["hit_tokens"] += int(ids.size)
|
| 219 |
+
s["per_task"] = [a + c for a, c in zip(s["per_task"], per_task_counts(bits))]
|
| 220 |
+
s["ordinals"].append(idx)
|
| 221 |
+
if h * 2 >= int(g.size):
|
| 222 |
+
s["hit_docs_over_half"] += 1
|
| 223 |
+
s["seconds"] = round(time.time() - t0, 1)
|
| 224 |
+
s["per_task_named"] = {n: c for n, c in zip(names, s["per_task"]) if c}
|
| 225 |
+
return s
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def doc_stream(BM, shard_paths):
|
| 229 |
+
"""(ordinal, raw document bytes) over a fixed list of shards. Ordinal order is the shard order in
|
| 230 |
+
the source's own record.json, which is exactly what the merge replays -- so an index computed here
|
| 231 |
+
still means the same document after a session restart."""
|
| 232 |
+
n = 0
|
| 233 |
+
for p in shard_paths:
|
| 234 |
+
for d in BM.iter_shard_docs(p):
|
| 235 |
+
yield n, d
|
| 236 |
+
n += 1
|
| 237 |
+
|
| 238 |
+
|
| 239 |
def main():
|
| 240 |
ap = argparse.ArgumentParser()
|
| 241 |
ap.add_argument("--root", default="/kaggle/working/mixroot")
|
| 242 |
+
ap.add_argument("--stage-subdir", default="stage",
|
| 243 |
+
help="per-source staged shards live here under --root; scanning them is what makes "
|
| 244 |
+
"attribution possible, because every merged shard contains all sources at once")
|
| 245 |
+
ap.add_argument("--max-items-per-split", type=int, default=50000)
|
| 246 |
+
ap.add_argument("--write-filter", default="",
|
| 247 |
+
help="directory for <source>.u8 exclusion masks + filter.json (read back by "
|
| 248 |
+
"build_mix --exclude-dir)")
|
| 249 |
ap.add_argument("--out", default="", help="write the report JSON here (publish_mix expects "
|
| 250 |
"mixroot/audit.json)")
|
| 251 |
a = ap.parse_args()
|
|
|
|
| 257 |
|
| 258 |
print("building reference gram table (only counts ever leave this process)", flush=True)
|
| 259 |
ref, refmask, refcounts, errors = build_reference(tk, a.max_items_per_split)
|
| 260 |
+
tasks_covered = sorted({k.split("::")[0] for k, v in refcounts.items()
|
| 261 |
+
if v >= MIN_ITEMS_PER_SPLIT})
|
| 262 |
+
print(f"reference: {ref.size:,} grams, {len(tasks_covered)}/8 tasks covered, "
|
| 263 |
+
f"{len(errors)} loader problems", flush=True)
|
| 264 |
|
|
|
|
| 265 |
import build_mix as BM
|
| 266 |
|
| 267 |
+
# ---- per-source attribution, over the staged (pre-merge) shards ------------------------------
|
| 268 |
+
# The first version attributed merged hits as `c // len(rec["sources"])` to every source named in the
|
| 269 |
+
# shard. All 141 shards name all 15 sources, so every source received an almost identical number and
|
| 270 |
+
# the table looked like a finding while being arithmetically meaningless.
|
| 271 |
+
stage_root = os.path.join(a.root, a.stage_subdir)
|
| 272 |
+
by_source, masks = {}, {}
|
| 273 |
+
if os.path.isdir(stage_root):
|
| 274 |
+
for key in sorted(os.listdir(stage_root)):
|
| 275 |
+
sd = os.path.join(stage_root, key)
|
| 276 |
+
rd = os.path.join(sd, "record.json")
|
| 277 |
+
if not os.path.isdir(sd) or not os.path.exists(rd):
|
| 278 |
+
continue
|
| 279 |
+
rec = json.load(open(rd))
|
| 280 |
+
paths = [os.path.join(a.root, s["file"]) for s in rec.get("shards", [])]
|
| 281 |
+
absent = [p for p in paths if not os.path.exists(p)]
|
| 282 |
+
if absent:
|
| 283 |
+
errors[f"stage::{key}"] = f"{len(absent)} staged shard(s) missing on disk"
|
| 284 |
+
print(f" stage {key}: {len(absent)} shard(s) missing, skipped", flush=True)
|
| 285 |
+
continue
|
| 286 |
+
s = scan_docs(ref, refmask, doc_stream(BM, paths))
|
| 287 |
+
s["shards"] = len(paths)
|
| 288 |
+
s["stage_complete"] = bool(rec.get("complete"))
|
| 289 |
+
by_source[key] = s
|
| 290 |
+
masks[key] = (np.asarray(sorted(s["ordinals"]), dtype=np.int64), s["docs"])
|
| 291 |
+
print(f" stage {key}: {s['docs']:,} docs {s['tokens']:,} tok -> hits={s['hits']:,} in "
|
| 292 |
+
f"{s['hit_docs']:,} docs ({s['hit_docs_over_half']:,} of them >50 % matched) "
|
| 293 |
+
f"[{s['seconds']}s] {json.dumps(s['per_task_named'])}", flush=True)
|
| 294 |
+
s.pop("ordinals")
|
| 295 |
+
else:
|
| 296 |
+
print(f"no staged sources under {stage_root}: attribution unavailable, totals only", flush=True)
|
| 297 |
+
|
| 298 |
+
# ---- the verdict is always measured on the bytes that will actually train ---------------------
|
| 299 |
+
man_path = os.path.join(a.root, "manifest.json")
|
| 300 |
hits = {}
|
| 301 |
+
docs = grams_seen = mix_tokens = hit_docs = hit_docs_over_half = 0
|
| 302 |
+
dup_docs = dup_tokens = 0
|
| 303 |
missing = []
|
| 304 |
+
if os.path.exists(man_path):
|
| 305 |
+
man = json.load(open(man_path))
|
| 306 |
+
for rec in man["shards"]:
|
| 307 |
+
p = os.path.join(a.root, rec["file"])
|
| 308 |
+
if not os.path.exists(p):
|
| 309 |
+
# A shard not on disk is unmeasured, not clean (E-017's shape).
|
| 310 |
+
missing.append(rec["file"])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
continue
|
| 312 |
+
s = scan_docs(ref, refmask, doc_stream(BM, [p]))
|
| 313 |
+
docs += s["docs"]
|
| 314 |
+
mix_tokens += s["tokens"]
|
| 315 |
+
grams_seen += s["grams"]
|
| 316 |
+
hit_docs += s["hit_docs"]
|
| 317 |
+
hit_docs_over_half += s["hit_docs_over_half"]
|
| 318 |
+
dup_docs += s["dup_docs"]
|
| 319 |
+
dup_tokens += s["dup_tokens"]
|
| 320 |
+
for n, c in s["per_task_named"].items():
|
| 321 |
+
hits[n] = hits.get(n, 0) + c
|
| 322 |
+
total_overlap = sum(hits.values())
|
| 323 |
+
print(f" mix: {docs:,} docs, {grams_seen:,} grams, {total_overlap:,} hits in {hit_docs:,} docs "
|
| 324 |
+
f"({hit_docs_over_half:,} majority-hit)", flush=True)
|
| 325 |
+
else:
|
| 326 |
+
total_overlap = 0
|
| 327 |
+
print(f"no manifest at {a.root}: merged mix not scanned", flush=True)
|
| 328 |
+
|
| 329 |
+
expected_fp = grams_seen * int(np.unique(ref).size) / float(1 << 64)
|
| 330 |
+
noise_allow = max(10.0, 10.0 * expected_fp)
|
| 331 |
+
per_task_named = {t[0]: hits.get(t[0], 0) for t in REFS}
|
| 332 |
+
|
| 333 |
+
if a.write_filter:
|
| 334 |
+
os.makedirs(a.write_filter, exist_ok=True)
|
| 335 |
+
summary = {"k": K, "criterion": "any 13-gram of the document appears in a reference "
|
| 336 |
+
"train/validation/dev split", "sources": {}}
|
| 337 |
+
for key, (ord_sorted, n_docs) in masks.items():
|
| 338 |
+
m = np.zeros(n_docs, dtype=np.uint8)
|
| 339 |
+
if ord_sorted.size:
|
| 340 |
+
m[ord_sorted] = 1
|
| 341 |
+
m.tofile(os.path.join(a.write_filter, key + ".u8"))
|
| 342 |
+
s = by_source[key]
|
| 343 |
+
summary["sources"][key] = {"docs": s["docs"], "tokens": s["tokens"],
|
| 344 |
+
"dropped_docs": int(ord_sorted.size),
|
| 345 |
+
"dropped_tokens": int(s["hit_tokens"]),
|
| 346 |
+
"kept_docs": int(n_docs - ord_sorted.size)}
|
| 347 |
+
with open(os.path.join(a.write_filter, "filter.json"), "w") as f:
|
| 348 |
+
json.dump(summary, f, indent=1, default=str)
|
| 349 |
+
print(f"filter masks written to {a.write_filter} for {len(masks)} source(s)", flush=True)
|
| 350 |
+
|
| 351 |
+
contaminated_sources = {k: v["hit_docs"] / max(1, v["docs"]) for k, v in by_source.items()
|
| 352 |
+
if v["hit_docs"]}
|
| 353 |
report = {
|
| 354 |
"k": K,
|
| 355 |
"fingerprint": "multiplicative rolling hash mod 2^64, P=0x100000001B3",
|
| 356 |
"reference_items_by_split": refcounts,
|
| 357 |
+
"reference_loader_problems": errors,
|
| 358 |
"reference_grams_distinct": int(np.unique(ref).size),
|
| 359 |
"reference_grams_total": int(ref.size),
|
| 360 |
+
"tasks_covered": tasks_covered,
|
| 361 |
"shards_missing": missing,
|
| 362 |
"mix_documents_audited": docs,
|
| 363 |
+
"mix_tokens_audited": mix_tokens,
|
| 364 |
"mix_grams_audited": grams_seen,
|
| 365 |
+
"mix_documents_with_any_hit": hit_docs,
|
| 366 |
+
"mix_documents_majority_hit": hit_docs_over_half,
|
| 367 |
+
"overlap_by_task": per_task_named,
|
| 368 |
"overlap_total": total_overlap,
|
| 369 |
+
"by_source": by_source,
|
| 370 |
+
"source_hit_document_fraction": {k: round(v, 6) for k, v in sorted(
|
| 371 |
+
contaminated_sources.items(), key=lambda kv: -kv[1])},
|
| 372 |
+
"duplicate_documents": dup_docs,
|
| 373 |
+
"duplicate_tokens": dup_tokens,
|
| 374 |
+
"expected_false_positives": round(expected_fp, 3),
|
| 375 |
+
"noise_allowance": round(noise_allow, 3),
|
| 376 |
+
"overlap_exceeds_noise": bool(total_overlap > noise_allow),
|
| 377 |
"note": ("counts only; no item text read or printed. Reference splits are train/validation/dev "
|
| 378 |
+
"per task -- never test, per §3.3. overlap_by_task sums gram hits, so one verbatim "
|
| 379 |
+
"benchmark document contributes as many hits as it has windows; read "
|
| 380 |
+
"mix_documents_majority_hit and source_hit_document_fraction to judge severity. "
|
| 381 |
+
"Duplicate counting is diagnostic only: de-duplication is out of scope (D-014), "
|
| 382 |
+
"contamination removal is not (§3.3)."),
|
| 383 |
+
"COVERED_ALL_SHARDS": bool(not missing and docs > 0),
|
| 384 |
+
"MEASURED": bool(docs > 0 and grams_seen > 0 and not errors and len(tasks_covered) == 8),
|
| 385 |
}
|
| 386 |
+
report["AUDIT_PASSED"] = bool(report["MEASURED"] and total_overlap <= noise_allow)
|
| 387 |
txt = json.dumps(report, indent=1, default=str)
|
| 388 |
+
print(f"VERDICT AUDIT_PASSED={report['AUDIT_PASSED']} MEASURED={report['MEASURED']} "
|
| 389 |
+
f"docs={docs:,} overlap={total_overlap} hit_docs={hit_docs:,} "
|
| 390 |
+
f"majority_hit_docs={hit_docs_over_half:,} expected_fp={expected_fp:.3f} "
|
| 391 |
+
f"tasks={len(tasks_covered)}/8 problems={len(errors)}")
|
| 392 |
print("AUDIT_JSON_BEGIN")
|
| 393 |
print(txt)
|
| 394 |
print("AUDIT_JSON_END")
|