#!/usr/bin/env python3 """Build deterministic, document-isolated OC-LM Strict-Small training epochs.""" from __future__ import annotations import argparse import hashlib import json from pathlib import Path import numpy as np from tokenizers import Tokenizer EXPECTED_WORDS = { "bnc_spoken.train.txt": 762_073, "childes.train.txt": 2_841_101, "gutenberg.train.txt": 2_557_721, "open_subtitles.train.txt": 2_282_877, "simple_wiki.train.txt": 1_531_437, "switchboard.train.txt": 24_791, } LENGTHS = (128,) * 6 + (256,) * 2 + (512,) * 2 SPECIAL = {"unk": 0, "bos": 1, "eos": 2, "pad": 3, "mask": 4} def sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(8 << 20), b""): digest.update(chunk) return digest.hexdigest() def flush_row(handles, tokens, segments, words, objective, length): if not tokens: return 0 used = len(tokens) tokens.extend([SPECIAL["pad"]] * (length - used)) segments.extend([0] * (length - used)) np.asarray(tokens, dtype=" None: parser = argparse.ArgumentParser() parser.add_argument("--raw", type=Path, required=True) parser.add_argument("--tokenizer", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--seed", type=int, default=20260904) parser.add_argument("--batch-lines", type=int, default=4096) parser.add_argument("--schedule", choices=["cycle", "fixed512"], required=True) args = parser.parse_args() args.output.mkdir(parents=True, exist_ok=False) schedule = (128,256,512,128,256,512,128,256,512,512) if args.schedule == "cycle" else (512,)*10 tokenizer = Tokenizer.from_file(str(args.tokenizer / "tokenizer.json")) if sha256(args.tokenizer / "tokenizer.json") != "98dfab9eabdd78aed27c025ec1bdbd881172c6339c0b491f9e1612e9a36fcbf8": raise RuntimeError("shared DST tokenizer hash mismatch") observed_special = { "unk": tokenizer.token_to_id(""), "bos": tokenizer.token_to_id(""), "eos": tokenizer.token_to_id(""), "pad": tokenizer.token_to_id(""), "mask": tokenizer.token_to_id(""), } if observed_special != SPECIAL or tokenizer.get_vocab_size() != 16_384: raise RuntimeError(f"tokenizer contract mismatch: {observed_special}") documents: list[list[int]] = [] word_counts: list[int] = [] raw_manifest = {} for path in sorted(args.raw.glob("*.txt")): expected = EXPECTED_WORDS.get(path.name) if expected is None: raise RuntimeError(f"unexpected raw file: {path.name}") file_words = 0 batch_text: list[str] = [] batch_words: list[int] = [] def emit_batch() -> None: if not batch_text: return encoded = [item.ids for item in tokenizer.encode_batch(batch_text, add_special_tokens=False)] for ids, count in zip(encoded, batch_words, strict=True): documents.append([SPECIAL["bos"], *ids, SPECIAL["eos"]]) word_counts.append(count) batch_text.clear() batch_words.clear() with path.open("r", encoding="utf-8") as handle: for raw_line in handle: text = raw_line.rstrip("\r\n") count = len(text.split()) if count == 0: continue file_words += count batch_text.append(text) batch_words.append(count) if len(batch_text) >= args.batch_lines: emit_batch() emit_batch() if file_words != expected: raise RuntimeError(f"word mismatch for {path.name}: {file_words} != {expected}") raw_manifest[path.name] = { "bytes": path.stat().st_size, "sha256": sha256(path), "words": file_words, } if sum(word_counts) != 10_000_000: raise RuntimeError("Strict-Small corpus must contain exactly 10,000,000 words") docs_token_count = sum(map(len, documents)) epochs = [] for epoch, length in enumerate(schedule): epoch_dir = args.output / f"epoch_{epoch:02d}_len_{length}" epoch_dir.mkdir(parents=True, exist_ok=True) paths = { name: epoch_dir / f"{name}.bin" for name in ("tokens", "segments", "lengths", "words", "objectives") } handles = {name: path.open("wb") for name, path in paths.items()} rng = np.random.default_rng(args.seed + epoch) order = rng.permutation(len(documents)) boundary_rng = np.random.default_rng(args.seed + 100000 + epoch) expected_stream = hashlib.sha256() row_tokens: list[int] = [] row_segments: list[int] = [] row_words = 0 segment = 0 rows = 0 for doc_index in order: doc = documents[int(doc_index)] expected_stream.update(np.asarray(doc, dtype=' length else None if first_size is not None and row_tokens: rows += flush_row(handles, row_tokens, row_segments, row_words, (rows + epoch) & 1, length) row_tokens, row_segments, row_words, segment = [], [], 0, 0 cursor = 0 while cursor < len(doc): room = length - len(row_tokens) if cursor == 0 and first_size is not None: room = min(room, first_size) take = min(room, len(doc) - cursor) row_tokens.extend(doc[cursor : cursor + take]) row_segments.extend([segment] * take) cursor += take if cursor == len(doc): row_words += word_counts[int(doc_index)] segment += 1 if len(row_tokens) == length or (first_size is not None and cursor == first_size): rows += flush_row( handles, row_tokens, row_segments, row_words, (rows + epoch) & 1, length, ) row_tokens, row_segments, row_words, segment = [], [], 0, 0 if row_tokens: rows += flush_row( handles, row_tokens, row_segments, row_words, (rows + epoch) & 1, length, ) for handle in handles.values(): handle.flush() handle.close() words = np.memmap(paths["words"], mode="r", dtype=" 1: raise RuntimeError(f"epoch {epoch} objective balance mismatch") epoch_record = { "epoch": epoch, "nonpad_stream_sha256": observed_stream.hexdigest(), "stream_readback": "PASS_EXACT_ORDER_CONTENT_NO_OMISSIONS", "sequence_length": length, "rows": rows, "nonpad_token_positions": int(lengths.sum(dtype=np.uint64)), "raw_words": int(words.sum(dtype=np.uint64)), "causal_sequences": int((objectives == 0).sum()), "mntp_sequences": int((objectives == 1).sum()), "files": { name: {"bytes": path.stat().st_size, "sha256": sha256(path)} for name, path in paths.items() }, } (epoch_dir / "manifest.json").write_text( json.dumps(epoch_record, indent=2, sort_keys=True) + "\n", encoding="utf-8" ) epochs.append(epoch_record) print(json.dumps(epoch_record, sort_keys=True), flush=True) manifest = { "schema_version": 1, "builder": "DST_CYCLIC_ENRICHED_100M_20260908/scripts/build_schedule_data.py", "boundary_policy": "Long-document first chunk length uniform 1..sequence_length each epoch; all prefixes/tails retained; document-isolated causal masks", "builder_sha256": sha256(Path(__file__)), "schedule": args.schedule, "seed": args.seed, "raw": raw_manifest, "documents": len(documents), "words_per_pass": sum(word_counts), "token_positions_per_pass": docs_token_count, "tokenizer": { "path": str(args.tokenizer), "tokenizer_json_sha256": sha256(args.tokenizer / "tokenizer.json"), "tokenizer_config_sha256": sha256(args.tokenizer / "tokenizer_config.json"), "vocab_size": tokenizer.get_vocab_size(), "special_ids": observed_special, }, "epochs": epochs, } manifest_path = args.output / "dataset_manifest.json" manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8") print(f"manifest_sha256={sha256(manifest_path)}") if __name__ == "__main__": main()