File size: 38,609 Bytes
20765b9 9c74162 20765b9 9f37a2c 20765b9 18d6af8 20765b9 18d6af8 20765b9 18d6af8 20765b9 a2bfa41 a067bd1 20765b9 a2bfa41 a067bd1 20765b9 a067bd1 a2bfa41 a067bd1 20765b9 a3bf1dd 20765b9 9e79358 a3bf1dd 9e79358 8138839 9e79358 8138839 9e79358 a3bf1dd 20765b9 a3bf1dd 20765b9 a3bf1dd 20765b9 a3bf1dd 20765b9 a3bf1dd 20765b9 a3bf1dd 20765b9 a2bfa41 9e79358 20765b9 9f37a2c 20765b9 9f37a2c 20765b9 9f37a2c 20765b9 9f37a2c 20765b9 a067bd1 20765b9 a3bf1dd 9e79358 20765b9 a067bd1 a2bfa41 a067bd1 a2bfa41 a067bd1 20765b9 a067bd1 a2bfa41 a067bd1 20765b9 7fd3601 9f37a2c 20765b9 9f37a2c 20765b9 a3bf1dd 20765b9 7fd3601 20765b9 7fd3601 20765b9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 | # Phase 2 builder: sources -> per-source staged shards -> interleaved mixed shards.
#
# Two stages, because one stage cannot produce a mixed corpus without re-streaming.
#
# stage per source: stream -> normalise -> language/quality gate -> dedup -> tokenise
# -> write stage/<key>/shard-####.bin
# merge read all staged shards together, emit documents round-robin in proportion to each source's
# target, and write the final mixed mix/shard-####.bin
#
# Why the merge exists (memory/ERRORS.md E-016). The first version consumed one source to its target and
# only then started the next, and it flushed a shard when the buffer filled. Since fineweb-edu's target is
# 300M tokens = 37 shards of 8M, the first 37 shards of the mix would have been nothing but web text, then
# textbooks, then math. A shuffle inside the buffer cannot fix that -- the buffer only ever held one
# source. That is a curriculum drift across the first third of the main run, and no LR schedule repairs
# ordering like that. Mixing must happen between sources, not inside them.
#
# Shard layout is shared by both stages and carries a per-document offset table, so document boundaries
# survive to the training reader (E-016's second latent bug: the old format stored a doc *count*, from
# which boundaries cannot be recovered):
#
# header : <IIII = vocab, n_docs, n_tokens, flags(0) 16 B
# offsets : uint32 × (n_docs + 1), cumulative, [0] = 0
# ids : uint16 × n_tokens
#
# doc i of a shard is ids[offsets[i] : offsets[i+1]].
#
# Constraints (docs/00-platform-notes.md): Kaggle CPU session, 4 vCPU, 30 GiB cgroup, **no swap**,
# 19.5 GB /kaggle/working. Staged (~2.6 GB) + final (~2.6 GB) + hash sets (~35 MB) fit with room. Streaming
# is ~3.3 MB/s per process and tokenising ~661k tok/s on 4 cores, so ingest is serial-per-source by design.
#
# Resumability: state is saved only *after* a file lands, and the dedup hash sets are appended in the same
# transaction -- otherwise a resume forgets what was already emitted and lets duplicates back in across
# the boundary. No credentials in this file.
import argparse
import array
import hashlib
import json
import os
import random
import struct
import sys
import time
# Sibling modules (hubsync, ounce100m_credentials) live beside this file, not in any package, and a job
# started as `python build/build_mix.py` gets only the script's own directory on sys.path -- which is
# build/, so the credentials import inside hubsync fails and the whole run refuses to be interruption-
# safe (E-021's lesson, hit again from the other direction). Add both this directory and its parent.
_HERE = os.path.dirname(os.path.abspath(__file__))
for _p in (_HERE, os.path.dirname(_HERE)):
if _p not in sys.path:
sys.path.insert(0, _p)
MAGIC_FLAGS = 0
TOKENS_PER_SHARD = 8_000_000 # 16 MB as uint16; ~140 shards for 1.1B tokens
MERGE_BATCH_DOCS = 32 # granularity of the round-robin; ~20k tokens, so a 262k batch spans ~13
VAL_STRIDE = 50 # every 50th document is held out ~= 2% of tokens, sampled across
# sources rather than whole documents, so validation PPL is honest
VAL_TOKENS_PER_SHARD = 2_000_000
ROW_TOKEN_CAP = 16_384 # one document must never dominate a shard
SHUFFLE_SEED = 20260919 # recorded so a rebuild is reproducible
HASHES_FILE = "seen_hashes.bin"
KEYS_FILE = "seen_keys.bin"
STATE_NAME = "build_state.json"
# Corrected config and column names, from the Phase 2 inventory (docs/02-mix-plan.md §9). The names in
# §2 came from dataset cards, and five of them did not exist.
SOURCES = [
dict(repo="HuggingFaceFW/fineweb-edu", config="sample-10BT", col="text", tokens=340_000_000,
min_score=4, why="quality-scored general English"),
dict(repo="HuggingFaceFW/finepdfs-edu", config="eng_Latn", col="text", tokens=180_000_000,
en_filter=True, why="long-form educational PDFs; needs own en filter"),
dict(repo="HuggingFaceTB/cosmopedia", config="stanford", col="text", tokens=60_000_000,
why="textbook expository"),
dict(repo="HuggingFaceTB/cosmopedia", config="openstax", col="text", tokens=30_000_000,
why="textbook science"),
dict(repo="HuggingFaceTB/cosmopedia", config="khanacademy", col="text", tokens=10_000_000,
why="worked steps"),
dict(repo="HuggingFaceTB/cosmopedia", config="auto_math_text", col="text", tokens=30_000_000,
why="synthetic math exposition"),
dict(repo="HuggingFaceTB/cosmopedia", config="wikihow", col="text", tokens=30_000_000,
why="procedural / physical-world steps"),
dict(repo="HuggingFaceFW/finewiki", config="en", col="rewritten", tokens=80_000_000,
why="rewritten encyclopedic prose"),
dict(repo="omarkamali/wikipedia-monthly", config="20260101.en", col="text", tokens=60_000_000,
en_filter=True, why="current encyclopedic breadth"),
dict(repo="HuggingFaceTB/finemath", config="finemath-4plus", col="text", tokens=160_000_000,
why="math; upstream-decontaminated against GSM8K/MMLU/ARC"),
dict(repo="open-web-math/open-web-math", config="default", col="text", tokens=70_000_000,
en_filter=True, why="forum/webbook math prose"),
dict(repo="HuggingFaceFW/finephrase", config="tutorial", col="text", tokens=60_000_000,
en_filter=True, why="stepwise tutorial register"),
dict(repo="HuggingFaceFW/finephrase", config="faq", col="text", tokens=30_000_000,
en_filter=True, why="FAQ register"),
dict(repo="HuggingFaceFW/finephrase", config="table", col="text", tokens=30_000_000,
en_filter=True, why="tabular->prose"),
dict(repo="SimpleStories/SimpleStories", config="default", col="story", tokens=40_000_000,
why="long-range simple narrative coherence"),
dict(repo="common-pile/arxiv_abstracts", config="default", col="text", tokens=20_000_000,
en_filter=True, why="CC0 scientific abstracts"),
# No code source yet: stack-v3-train has only a `default` config and its rows are whole repos with a
# nested files struct, so it needs its own probe (docs/02-mix-plan.md §9.3).
# common-pile/libretexts dropped: `text` holds URLs, 93% of sampled rows failed the en test.
]
STOP = (" the ", " and ", " of ", " to ", " that ", " is ", " in ", " for ", " with ", " as ")
KEY_COLS = ("url", "id", "file_path", "doc_id", "generation_id", "repo_name")
def src_key(src):
return f"{src['repo'].split('/')[1]}__{src['config']}"
def normalize(s):
# Whitespace-collapse only: aggressive folding collides genuinely different documents (tables, code),
# and a false merge is worse than a missed near-duplicate.
return " ".join(s.split())
def looks_english(s):
low = s[:600].lower()
return sum(w in low for w in STOP) >= 3
def h64(s):
"""64-bit digest of a string. digest()[:8] is 8 BYTES; hexdigest()[:16] is 16 *characters*
and broke the first version (E-015)."""
return struct.unpack("<Q", hashlib.sha256(s.encode("utf-8")).digest()[:8])[0]
def key_of(row, cols):
for c in KEY_COLS:
if c in cols:
v = row.get(c)
if isinstance(v, str) and v:
return v
return None
# --------------------------------------------------------------------- shard I/O
def write_shard(path, vocab, docs):
"""docs: iterable of array('H')/list of token ids. Returns (n_docs, n_tokens, sha256, bytes)."""
offs = array.array("I", [0])
ids = array.array("H")
for d in docs:
ids.extend(d)
offs.append(len(ids))
tmp = path + ".tmp"
with open(tmp, "wb") as f:
f.write(struct.pack("<IIII", vocab, len(offs) - 1, len(ids), MAGIC_FLAGS))
offs.tofile(f)
ids.tofile(f)
os.replace(tmp, path)
digest = hashlib.sha256(open(path, "rb").read()).hexdigest()
return len(offs) - 1, len(ids), digest, os.path.getsize(path)
def read_shard_header(path):
with open(path, "rb") as f:
vocab, n_docs, n_tokens, flags = struct.unpack("<IIII", f.read(16))
return vocab, n_docs, n_tokens, flags
def iter_shard_docs(path):
"""Yield documents as array('H'). Offsets are read in full (4 B/doc) which is fine at 8M tok/shard."""
with open(path, "rb") as f:
vocab, n_docs, n_tokens, _flags = struct.unpack("<IIII", f.read(16))
offs = array.array("I")
offs.fromfile(f, n_docs + 1)
ids = array.array("H")
ids.fromfile(f, n_tokens)
for i in range(n_docs):
yield ids[offs[i]:offs[i + 1]]
# --------------------------------------------------------------------- state
class State:
def __init__(self, root):
self.root = root
self.stage_dir = os.path.join(root, "stage")
self.mix_dir = os.path.join(root, "mix")
os.makedirs(self.stage_dir, exist_ok=True)
os.makedirs(self.mix_dir, exist_ok=True)
self.path = os.path.join(root, STATE_NAME)
self.hashes_path = os.path.join(root, HASHES_FILE)
self.keys_path = os.path.join(root, KEYS_FILE)
self.d = {"staged": {}, "merged_shards": [], "version": 3}
self.seen_exact = set()
self.seen_key = set()
if os.path.exists(self.path):
self.d = json.load(open(self.path))
self.seen_exact = self._load_u64(self.hashes_path)
self.seen_key = self._load_u64(self.keys_path)
print(f"resuming: {len(self.d['staged'])} sources staged, "
f"{len(self.d['merged_shards'])} mixed shards, "
f"{len(self.seen_exact):,} hashes", flush=True)
@staticmethod
def _load_u64(p):
if not os.path.exists(p):
return set()
a = array.array("Q")
with open(p, "rb") as f:
a.fromfile(f, os.path.getsize(p) // 8)
return set(a)
def commit(self, new_hashes, new_keys):
"""Append the dedup sets FIRST, then the state file: if the job dies between the two, the worst
case is a hash set slightly ahead of the state (a document re-emitted is a duplicate, which the
next run's dedup catches; a hash set behind the state is a permanent silent duplicate)."""
if new_hashes:
with open(self.hashes_path, "ab") as f:
array.array("Q", new_hashes).tofile(f)
if new_keys:
with open(self.keys_path, "ab") as f:
array.array("Q", new_keys).tofile(f)
tmp = self.path + ".tmp"
with open(tmp, "w") as f:
json.dump(self.d, f, indent=1)
os.replace(tmp, self.path)
def staged_tokens(self, key):
return sum(s["tokens"] for s in self.d["staged"].get(key, {}).get("shards", []))
def staged_tokens_total(self):
return sum(v.get("tokens", 0) for v in self.d["staged"].values())
def merged_tokens(self):
return sum(s["tokens"] for s in self.d["merged_shards"])
# --------------------------------------------------------------------- stage
def stage(args, st, tokenizer):
remote = HS.remote_stage_files(args.hub_repo) if (HS and args.hub_repo
and not args.merge_only) else set()
if args.hub_repo:
print(f"hub: {len(remote)} staged files already on {args.hub_repo}", flush=True)
for src in SOURCES:
key = src_key(src)
rec = st.d["staged"].get(key, {})
if rec.get("complete"):
print(f"stage: skip {key} (complete)", flush=True)
continue
if HS and args.hub_repo:
# A source whose shards AND record are already on the Hub is done; pulling the record is
# cheaper than re-staging and keeps the merge's proportional weights honest.
have = [p for p in remote if p.startswith(f"stage/{key}/") and p.endswith(".bin")]
if have and f"stage/{key}/record.json" in remote:
try:
HS.download_missing(st.root, [key], args.hub_repo, remote=remote)
rec_json = json.load(open(os.path.join(st.stage_dir, key, "record.json")))
st.d["staged"][key] = {"complete": True, "shards": rec_json["shards"],
"tokens": rec_json["tokens"],
"docs_seen": rec_json["docs_seen"],
"dropped": rec_json["dropped"],
"target": rec_json["tokens"], "restored_from_hub": True}
st.commit([], [])
print(f"stage: restored {key} from Hub ({rec_json['tokens']:,} tok), skipping",
flush=True)
continue
except Exception as e:
print(f"!! restore failed for {key}: {type(e).__name__}: {str(e)[:140]}", flush=True)
want = min(src["tokens"], args.cap_tokens_per_source or src["tokens"])
if args.budget_tokens and st.staged_tokens_total() >= args.budget_tokens:
print("stage: overall budget reached", flush=True)
break
import datasets
try:
ds = datasets.load_dataset(src["repo"], src["config"], split="train", streaming=True)
except Exception as e:
print(f"!! stage: {key} could not load: {type(e).__name__}: {str(e)[:200]}", flush=True)
rec["error"] = f"{type(e).__name__}: {str(e)[:200]}"
st.d["staged"][key] = rec
st.commit([], [])
continue
got = docs_seen = 0
drops = {"short_or_low_score": 0, "non_english": 0, "exact_dup": 0, "key_dup": 0,
"too_few_tokens": 0, "no_column": 0}
cols = None
buf, new_h, new_k = [], [], []
sd = os.path.join(st.stage_dir, key)
os.makedirs(sd, exist_ok=True)
shards = rec.get("shards", [])
t0 = time.monotonic()
for row in ds:
if got >= want:
break
if args.sec_per_source and time.monotonic() - t0 > args.sec_per_source:
break
if cols is None:
cols = set(row.keys())
if src["col"] not in cols:
print(f"!! stage: {key} has no column {src['col']!r}; seen {sorted(cols)[:10]}",
flush=True)
drops["no_column"] = -1
break
raw = row.get(src["col"])
if not isinstance(raw, str):
continue
docs_seen += 1
s = normalize(raw)
if len(s) < args.min_chars:
drops["short_or_low_score"] += 1
continue
if src.get("min_score") is not None:
sc = row.get("int_score", row.get("score"))
try:
if sc is not None and float(sc) < src["min_score"]:
drops["short_or_low_score"] += 1
continue
except (TypeError, ValueError):
pass
if src.get("en_filter") and not looks_english(s):
drops["non_english"] += 1
continue
h = h64(s)
if h in st.seen_exact:
drops["exact_dup"] += 1
continue
u = key_of(row, cols)
ku = h64(u) if u else None
if ku is not None and ku in st.seen_key:
drops["key_dup"] += 1
continue
ids = tokenizer.encode(s[:args.max_chars], add_special_tokens=False).ids[:ROW_TOKEN_CAP]
if len(ids) < 16:
drops["too_few_tokens"] += 1
continue
st.seen_exact.add(h)
new_h.append(h)
if ku is not None:
st.seen_key.add(ku)
new_k.append(ku)
buf.append(array.array("H", ids))
got += len(ids)
if sum(len(b) for b in buf) >= TOKENS_PER_SHARD:
idx = len(shards)
p = os.path.join(sd, f"shard-{idx:04d}.bin")
nd, nt, dg, bs = write_shard(p, tokenizer.get_vocab_size(), buf)
shards.append({"file": os.path.relpath(p, st.root), "docs": nd, "tokens": nt,
"sha256": dg, "bytes": bs})
st.seen_exact.update(new_h)
st.d["staged"][key] = {"complete": False, "shards": shards, "tokens": got,
"docs_seen": docs_seen, "dropped": drops}
st.commit(new_h, new_k)
new_h, new_k = [], []
buf = []
print(f" staged {key} shard {idx:04d}: {nt:,} tok [{time.monotonic() - t0:.0f}s]",
flush=True)
if buf:
idx = len(shards)
p = os.path.join(sd, f"shard-{idx:04d}.bin")
nd, nt, dg, bs = write_shard(p, tokenizer.get_vocab_size(), buf)
shards.append({"file": os.path.relpath(p, st.root), "docs": nd, "tokens": nt,
"sha256": dg, "bytes": bs})
print(f" staged {key} shard {idx:04d}: {nt:,} tok (tail)", flush=True)
complete = got >= want
st.d["staged"][key] = {"complete": bool(complete), "shards": shards, "tokens": got,
"docs_seen": docs_seen, "dropped": dict(drops),
"target": want, "seconds": round(time.monotonic() - t0, 1)}
st.commit(new_h, new_k)
# /kaggle/working is destroyed when the session ends, so a source that took 40 minutes to stage
# must not exist only there. Published per source, because that is the unit a resume can skip.
if complete and HS and args.hub_repo:
with open(os.path.join(st.stage_dir, key, "record.json"), "w") as f:
json.dump({"key": key, "src": {k: v for k, v in src.items()},
"shards": shards, "tokens": got, "docs_seen": docs_seen,
"dropped": dict(drops)}, f, indent=1)
try:
n = HS.upload_source(st.root, key, args.hub_repo)
print(f" published {key} -> {args.hub_repo}/stage/{key} ({n} files)", flush=True)
except Exception as e:
print(f"!! publish FAILED for {key}: {type(e).__name__}: {str(e)[:160]} -- "
f"this source will have to be re-staged if the session dies", flush=True)
el = time.monotonic() - t0
print(f"stage: {key} {got:,} tok of {want:,} from {docs_seen:,} docs; "
f"dup={drops['exact_dup']} keydup={drops['key_dup']} lang={drops['non_english']} "
f"short={drops['short_or_low_score']} [{got / max(1e-9, el):,.0f} tok/s]", flush=True)
# --------------------------------------------------------------------- merge
def merge(args, st, tokenizer):
"""Interleave staged documents proportionally to each source's staged token total.
--exclude-dir applies per-source uint8 masks written by audit_contamination --write-filter: a 1 byte
means "this document's 13-grams appear in a benchmark train/validation/dev split, do not emit it"
(§3.3). Masks are indexed by document ordinal within the source, which is stable because both the
audit and the merge replay the source's own record.json shard order.
"""
vocab = tokenizer.get_vocab_size()
masks, dropped, filter_digest = {}, {}, None
if args.exclude_dir:
fj = os.path.join(args.exclude_dir, "filter.json")
if not os.path.exists(fj):
print(f"FATAL: --exclude-dir {args.exclude_dir} contains no filter.json", flush=True)
sys.exit(5)
filter_digest = hashlib.sha256(open(fj, "rb").read()).hexdigest()
fj_obj = json.load(open(fj))
if not fj_obj.get("scanned_sources") and not fj_obj.get("sources"):
print("FATAL: filter.json names no sources at all -- nothing was scanned, so merging with it "
"would publish an unfiltered mix under a filter claim", flush=True)
sys.exit(5)
if not fj_obj.get("scanned_sources"):
# The completeness field post-dates some archived masks. It is a proxy; the real invariant is
# "a mask exists for every source this merge will read", which is checked per-source below.
print(f"WARNING: filter.json predates the scanned_sources field; trusting its "
f"{len(fj_obj.get('sources', {}))} per-source entries because every merged source must "
"still carry a mask or the merge refuses", flush=True)
dropped = fj_obj.get("sources", {})
for key in sorted(os.listdir(args.exclude_dir)):
if key.endswith(".u8"):
with open(os.path.join(args.exclude_dir, key), "rb") as fh:
masks[key[:-3]] = fh.read()
print(f"exclude: {len(masks)} mask(s), "
f"{sum(len(m) for m in masks.values()):,} docs covered, "
f"{sum(int(v.get('dropped_docs', 0)) for v in dropped.values()):,} flagged", flush=True)
streams = []
for src in SOURCES:
key = src_key(src)
rec = st.d["staged"].get(key)
if not rec or not rec.get("shards"):
continue
staged_docs = sum(int(s.get("docs", 0)) for s in rec["shards"])
m = masks.get(key)
if m is not None and len(m) != staged_docs:
# A short mask would silently keep exactly the documents the audit did not reach.
print(f"FATAL: mask for {key} covers {len(m)} docs but {staged_docs} are staged", flush=True)
sys.exit(5)
weight = rec.get("tokens") or sum(s["tokens"] for s in rec["shards"])
weight = weight - int((dropped.get(key) or {}).get("dropped_tokens", 0))
if weight <= 0:
print(f" {key}: fully excluded, dropped from the merge", flush=True)
continue
iters = [iter_shard_docs(os.path.join(st.root, s["file"])) for s in rec["shards"]]
def gen(its=iters, m=m):
if m is None:
for it in its:
for d in it:
yield d
return
i = 0
for it in its:
for d in it:
if not m[i]:
yield d
i += 1
streams.append({"key": key, "weight": float(weight), "credit": 0.0, "it": gen(iters),
"pending": None, "emitted_tokens": 0, "exhausted": False})
if not streams:
# The cold-resume rehearsal exited 0 here while producing an empty mix, and verify_mix then
# reported recount_matches_manifest: true because 0 == 0. An empty mix is the worst possible
# silent success in this pipeline, so it is a failure and not a return.
print("FATAL merge: nothing staged. Refusing to emit an empty mix.", flush=True)
sys.exit(4)
if args.exclude_dir:
# A missing mask must never mean "this source is clean" (review finding): it means the audit
# never scanned it, and the default behaviour would have been to merge every contaminated doc.
unscanned = [s["key"] for s in streams if s["key"] not in masks]
if unscanned:
print(f"FATAL merge: --exclude-dir has no mask for {unscanned}. Refusing to merge sources "
"the contamination audit did not read.", flush=True)
sys.exit(5)
# A re-merge is only correct if it starts from nothing. The loop below stops when merged_tokens()
# reaches the input total, so on a mixroot that already holds 141 shards it would emit ZERO new
# shards, exit 0, and let the manifest claim a filter that never happened (found in review while
# Gate 2c was running against exactly this path). Resume-after-a-kill stays legal -- it is the same
# inputs -- so the test is on a digest of the merge's inputs, not on whether shards exist.
digest = hashlib.sha256(json.dumps({
"streams": [[s["key"], round(s["weight"]) ] for s in streams], "seed": SHUFFLE_SEED,
"tokens_per_shard": TOKENS_PER_SHARD, "val_stride": VAL_STRIDE,
"filter": filter_digest}, sort_keys=True).encode()).hexdigest()
prior = st.d.get("merge_inputs")
if prior is not None and prior != digest and st.d["merged_shards"]:
if not (args.rebuild_merged or args.exclude_dir):
print("FATAL merge: the mix root already holds shards merged from DIFFERENT inputs "
f"(recorded {prior[:12]}, now {digest[:12]}). Continuing would be a silent no-op. "
"Pass --rebuild-merged to discard them deliberately.", flush=True)
sys.exit(6)
n_old = len(st.d["merged_shards"]) + len(st.d.get("val_shards", []))
for grp in ("merged_shards", "val_shards"):
for rec in st.d.get(grp, []):
p = os.path.join(st.root, rec["file"])
if os.path.exists(p):
os.remove(p)
st.d["merged_shards"] = []
st.d["val_shards"] = []
print(f"merge: inputs changed ({prior[:12]} -> {digest[:12]}); discarded {n_old} old shard(s) "
"and rebuilding from the staged sources", flush=True)
st.d["merge_inputs"] = digest
total_w = sum(s["weight"] for s in streams)
print(f"merge: {len(streams)} sources, "
f"{sum(s['weight'] for s in streams) / 1e6:,.0f}M staged tokens", flush=True)
def next_doc(s):
if s["pending"] is None and not s["exhausted"]:
try:
s["pending"] = next(s["it"])
except StopIteration:
s["exhausted"] = True
return s["pending"]
cap = args.merge_budget_tokens or int(total_w)
buf, buf_src, buf_tokens = [], [], 0
val_buf, val_tokens, docs_seq = [], 0, 0
out_dir = os.path.join(st.root, "mix")
os.makedirs(out_dir, exist_ok=True)
t0 = time.monotonic()
def flush_val():
nonlocal val_buf, val_tokens
if not val_buf:
return
idx = len(st.d.get("val_shards", []))
p = os.path.join(st.root, "val", f"shard-{idx:04d}.bin")
os.makedirs(os.path.dirname(p), exist_ok=True)
tmp = p + ".tmp"
offs = array.array("I", [0])
ids = array.array("H")
for d in val_buf:
ids.extend(d)
offs.append(len(ids))
with open(tmp, "wb") as f:
f.write(struct.pack("<IIII", vocab, len(val_buf), len(ids), MAGIC_FLAGS))
offs.tofile(f)
ids.tofile(f)
os.replace(tmp, p)
st.d.setdefault("val_shards", []).append({
"file": os.path.relpath(p, st.root), "docs": len(val_buf), "tokens": len(ids),
"sha256": hashlib.sha256(open(p, "rb").read()).hexdigest(), "bytes": os.path.getsize(p)})
st.commit([], [])
print(f" val shard {idx:04d}: {len(ids):,} tok / {len(val_buf):,} docs", flush=True)
val_buf, val_tokens = [], 0
def flush():
nonlocal buf, buf_src, buf_tokens
if not buf:
return
random.Random(SHUFFLE_SEED + len(st.d["merged_shards"])).shuffle(buf)
docs = [d for d, _ in buf]
p = os.path.join(out_dir, f"shard-{len(st.d['merged_shards']):04d}.bin")
nd, nt, dg, bs = write_shard(p, vocab, docs)
st.d["merged_shards"].append({"file": os.path.relpath(p, st.root), "docs": nd, "tokens": nt,
"sha256": dg, "bytes": bs,
"sources": sorted(set(s for _, s in buf))})
st.commit([], [])
print(f" mix shard {len(st.d['merged_shards']) - 1:04d}: {nt:,} tok / {nd:,} docs "
f"from {len(st.d['merged_shards'][-1]['sources'])} sources "
f"[{time.monotonic() - t0:.0f}s]", flush=True)
buf, buf_src, buf_tokens = [], [], 0
while st.merged_tokens() + buf_tokens < cap:
live = [s for s in streams if next_doc(s) is not None]
if not live:
break
# Largest-deficit scheduling: whoever is furthest below its proportional share of what has been
# emitted goes next. O(n_sources) per batch of MERGE_BATCH_DOCS, not per document.
emitted = sum(s["emitted_tokens"] for s in streams) or 1
pick = max(live, key=lambda s: s["weight"] / total_w - s["emitted_tokens"] / emitted)
took = 0
while took < MERGE_BATCH_DOCS:
d = next_doc(pick)
if d is None:
break
docs_seq += 1
if docs_seq % VAL_STRIDE == 0:
# held out of training entirely: routed to the validation split instead of the buffer
val_buf.append(d)
val_tokens += len(d)
if val_tokens >= VAL_TOKENS_PER_SHARD:
flush_val()
pick["emitted_tokens"] += len(d)
pick["pending"] = None
took += 1
continue
buf.append((d, pick["key"]))
buf_tokens += len(d)
pick["emitted_tokens"] += len(d)
pick["pending"] = None
took += 1
if buf_tokens >= TOKENS_PER_SHARD:
flush()
break
if all(s["pending"] is None and s["exhausted"] for s in streams):
break
flush()
flush_val()
print(f"merge: {len(st.d['val_shards']) if 'val_shards' in st.d else 0} val shards, "
f"{sum(s['tokens'] for s in st.d.get('val_shards', [])):,} held-out tokens", flush=True)
print(f"merge: {len(st.d['merged_shards'])} shards, "
f"{sum(s['tokens'] for s in st.d['merged_shards']):,} tokens", flush=True)
# --------------------------------------------------------------------- entry
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--root", default="/kaggle/working/mixroot")
ap.add_argument("--budget-tokens", type=int, default=1_310_000_000, help="stage cap")
ap.add_argument("--merge-budget-tokens", type=int, default=0, help="0 = take everything staged")
ap.add_argument("--cap-tokens-per-source", type=int, default=0, help="smoke test knob")
ap.add_argument("--min-chars", type=int, default=300)
ap.add_argument("--max-chars", type=int, default=100_000)
ap.add_argument("--sec-per-source", type=int, default=0)
ap.add_argument("--only", default="", help="comma list of config names to stage")
ap.add_argument("--hub-repo", default="Cion-lab/ounce100m-mix-stage",
help="staged sources are published here so a session kill cannot lose them; "
"empty string disables sync")
ap.add_argument("--stage-only", action="store_true")
ap.add_argument("--merge-only", action="store_true")
ap.add_argument("--exclude-dir", default="",
help="directory of per-source <key>.u8 contamination masks + filter.json written by "
"audit_contamination --write-filter; documents flagged there are not merged (§3.3)")
ap.add_argument("--rebuild-merged", action="store_true",
help="discard shards already recorded in build_state.json before merging. Implied by "
"--exclude-dir, because a filter changes every shard's contents")
args = ap.parse_args()
if args.only:
keep = set(args.only.split(","))
SOURCES[:] = [s for s in SOURCES if s["config"] in keep or src_key(s) in keep]
print(f"--only: {len(SOURCES)} source(s)", flush=True)
from tokenizers import Tokenizer
from huggingface_hub import hf_hub_download
tok_path = hf_hub_download("HuggingFaceTB/SmolLM2-135M", "tokenizer.json")
tk = Tokenizer.from_file(tok_path)
tk.no_truncation()
assert tk.get_vocab_size() <= 65536, "uint16 shard layout breaks above 65,536"
print(f"tokenizer vocab={tk.get_vocab_size()} "
f"sha256={hashlib.sha256(open(tok_path, 'rb').read()).hexdigest()[:12]}", flush=True)
global HS
HS = None
# Sync is best-effort to SET UP but not to USE: if the Hub cannot be reached or written, turning it
# off silently would reintroduce the exact failure this exists to prevent (a killed session losing
# hours of streaming). So a configured repo that cannot be initialised is fatal unless --no-hub.
if args.hub_repo:
try:
import hubsync as HS_mod
HS = HS_mod
HS.ensure_repo(args.hub_repo)
except Exception as e:
print(f"FATAL hub sync could not initialise: {type(e).__name__}: {str(e)[:200]}",
flush=True)
print("Refusing to run a multi-hour build that is not interruption-safe. Resolve the Hub "
"write path, or pass --hub-repo '' only if you accept losing all staged work when the "
"session ends.", flush=True)
sys.exit(3)
st = State(args.root)
if args.hub_repo and not st.d["staged"]:
# The state file lives on /kaggle/working too, so in a fresh session it is empty even when the
# Hub holds a complete stage. Rebuild the stage bookkeeping from the Hub's record.json files --
# without this, --merge-only on a new instance would find nothing and quietly emit an empty mix.
try:
remote = HS.remote_stage_files(args.hub_repo)
keys = sorted({p.split("/")[1] for p in remote if p.startswith("stage/") and "/" in p})
pulled = HS.download_missing(args.root, keys, args.hub_repo, remote=remote)
for k in keys:
rp = os.path.join(args.root, "stage", k, "record.json")
if not os.path.exists(rp):
continue
j = json.load(open(rp))
st.d["staged"][k] = {"complete": True, "shards": j["shards"], "tokens": j["tokens"],
"docs_seen": j["docs_seen"], "dropped": j["dropped"],
"target": j["tokens"], "restored_from_hub": True}
st.commit([], [])
print(f"hub: restored {len(st.d['staged'])} staged source(s) "
f"({st.staged_tokens_total():,} tokens)", flush=True)
except Exception as e:
print(f"FATAL hub restore failed: {type(e).__name__}: {str(e)[:200]}", flush=True)
sys.exit(3)
t0 = time.monotonic()
if not args.merge_only:
stage(args, st, tk)
if not args.stage_only:
merge(args, st, tk)
manifest = {
"tokenizer": {"id": "HuggingFaceTB/SmolLM2-135M:tokenizer.json",
"vocab_size": tk.get_vocab_size(),
"sha256": hashlib.sha256(open(tok_path, "rb").read()).hexdigest()},
"layout": {"dtype": "uint16", "header": "<IIII (vocab, n_docs, n_tokens, flags)",
"offsets": "uint32 × (n_docs+1) cumulative",
"doc_slice": "ids[offsets[i]:offsets[i+1]]",
"tokens_per_shard_target": TOKENS_PER_SHARD, "row_token_cap": ROW_TOKEN_CAP,
"merge_batch_docs": MERGE_BATCH_DOCS, "shuffle_seed_base": SHUFFLE_SEED,
"resume_cursor": "(shard_index, token_offset) into the concatenated id stream"},
"stage": {k: {kk: vv for kk, vv in v.items() if kk != "shards"}
for k, v in st.d["staged"].items()},
"staged_tokens": st.staged_tokens_total(),
"shards": st.d["merged_shards"],
"n_shards": len(st.d["merged_shards"]),
"val_shards": st.d.get("val_shards", []),
"total_tokens": sum(s["tokens"] for s in st.d["merged_shards"]),
"val_tokens": sum(s["tokens"] for s in st.d.get("val_shards", [])),
"val_holdout": {"stride": VAL_STRIDE, "note": "every 50th merged document, sampled across "
"sources rather than whole documents, excluded from training shards"},
"distinct_sources_per_shard_min": min((len(s["sources"]) for s in st.d["merged_shards"]),
default=0),
"seconds": round(time.monotonic() - t0, 1),
}
if args.exclude_dir:
# Provenance: a filtered mix is only reproducible if the exclusion set travels with it, so the
# counts go in the manifest and the mask bytes go in mixroot/filter/ (publish_mix ships them).
try:
fj = json.load(open(os.path.join(args.exclude_dir, "filter.json")))
manifest["contamination_filter"] = {
"criterion": fj.get("criterion"), "k": fj.get("k"),
"docs_dropped": sum(int(v.get("dropped_docs", 0)) for v in fj["sources"].values()),
"tokens_dropped": sum(int(v.get("dropped_tokens", 0) or 0)
for v in fj["sources"].values()),
"per_source": {k2: {"docs": v2.get("dropped_docs"),
"tokens": v2.get("dropped_tokens")}
for k2, v2 in fj["sources"].items()}}
except Exception as e:
print(f"FATAL: --exclude-dir set but filter.json unreadable: {e}", flush=True)
sys.exit(5)
with open(os.path.join(args.root, "manifest.json"), "w") as f:
json.dump(manifest, f, indent=1)
# Nothing may be printed between the JSON and its END marker: the caller parses this span with
# json.loads, and a stray status line turned a passing rehearsal red (E-019).
print("BUILD_JSON_BEGIN")
print(json.dumps({k: manifest[k] for k in
("staged_tokens", "total_tokens", "val_tokens", "n_shards",
"distinct_sources_per_shard_min", "seconds")}, indent=1))
print("BUILD_JSON_END")
if __name__ == "__main__":
main()
|