| """Build the SmolLM-corpus 75/15/10 mix into verifiable uint16 shards. |
| |
| Adopts the SmolLM data recipe (proven sub-1B winner over Pythia-410M / |
| MobileLLM-350M / Qwen2-500M) but re-tokenized through our locked GPT-2 BPE so |
| that loader, shard format, and tokenizer-comparability with v1 are unchanged. |
| |
| Mix targets are token-based, not document-based: documents have wildly |
| different lengths, so picking-by-doc-count would drift far from the intended |
| mix. We pick from whichever source is most under-represented in *tokens* |
| emitted so far, which converges to exactly the configured weights. |
| |
| Default 15B-token target matches configs/base_350m.json. Streams the three |
| HuggingFaceTB/smollm-corpus subsets so disk only holds the tokenized output. |
| |
| Usage (on the GPU instance): |
| |
| python scripts/prepare_smollm_data.py \ |
| --target-tokens 15_000_000_000 \ |
| --out-dir data/smollm_mix |
| """ |
|
|
| from __future__ import annotations |
|
|
| import sys |
| import argparse |
| import logging |
| from pathlib import Path |
|
|
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) |
|
|
| from matilda.data import ShardWriter, verify_manifest |
|
|
| log = logging.getLogger("prepare_smollm_data") |
|
|
|
|
| SOURCES: tuple[tuple[str, float], ...] = ( |
| ("fineweb-edu-dedup", 0.75), |
| ("cosmopedia-v2", 0.15), |
| ("python-edu", 0.10), |
| ) |
| DATASET = "HuggingFaceTB/smollm-corpus" |
| TEXT_KEY = "text" |
|
|
|
|
| def _open_stream(name: str): |
| from datasets import load_dataset |
| return iter(load_dataset(DATASET, name=name, split="train", streaming=True)) |
|
|
|
|
| def _pick_source(accumulated: dict[str, int]) -> str: |
| """Return the source whose token deficit relative to its weight is largest. |
| Equivalent to argmin(accumulated[s] / weight[s]) over s in SOURCES.""" |
| return min( |
| (name for name, _ in SOURCES), |
| key=lambda n: accumulated[n] / next(w for s, w in SOURCES if s == n), |
| ) |
|
|
|
|
| def main() -> None: |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--target-tokens", type=int, default=15_000_000_000) |
| ap.add_argument("--shard-tokens", type=int, default=100_000_000) |
| ap.add_argument("--out-dir", default="data/smollm_mix") |
| ap.add_argument("--tokenizer", default="gpt2") |
| ap.add_argument("--log-every", type=int, default=1000) |
| args = ap.parse_args() |
|
|
| logging.basicConfig(level=logging.INFO, format="%(message)s") |
| import tiktoken |
|
|
| enc = tiktoken.get_encoding(args.tokenizer) |
| eot = enc.eot_token |
| assert enc.n_vocab <= 65535, "vocab > uint16; loader assumes uint16 shards" |
|
|
| streams = {name: _open_stream(name) for name, _ in SOURCES} |
| accumulated = {name: 0 for name, _ in SOURCES} |
| n_docs = {name: 0 for name, _ in SOURCES} |
| writer = ShardWriter(args.out_dir, shard_tokens=args.shard_tokens) |
|
|
| while writer.total_tokens < args.target_tokens: |
| pick = _pick_source(accumulated) |
| try: |
| doc = next(streams[pick]) |
| except StopIteration: |
| |
| |
| |
| log.warning("source %s exhausted at %d tokens; restarting stream", |
| pick, writer.total_tokens) |
| streams[pick] = _open_stream(pick) |
| doc = next(streams[pick]) |
|
|
| ids = enc.encode_ordinary(doc[TEXT_KEY]) |
| ids.append(eot) |
| writer.add(ids) |
| accumulated[pick] += len(ids) |
| n_docs[pick] += 1 |
|
|
| total_docs = sum(n_docs.values()) |
| if total_docs % args.log_every == 0: |
| mix = {n: f"{accumulated[n] / max(1, writer.total_tokens) * 100:5.2f}%" |
| for n in accumulated} |
| log.info("docs=%d tokens=%d mix=%s", |
| total_docs, writer.total_tokens, mix) |
|
|
| manifest = writer.close(meta={ |
| "dataset": DATASET, |
| "splits": [name for name, _ in SOURCES], |
| "weights": {name: weight for name, weight in SOURCES}, |
| "tokenizer": args.tokenizer, |
| "eot_token": eot, |
| "n_docs_per_source": n_docs, |
| "tokens_per_source": accumulated, |
| }) |
| log.info("wrote %d tokens in %d shards -> %s", |
| manifest["total_tokens"], len(manifest["shards"]), args.out_dir) |
| verify_manifest(args.out_dir) |
| log.info("manifest verified (checksums + sizes OK)") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|