Download ar/data/prepare_data.py from AjayRangoji/AR: direct link, hf CLI and curl.
- Browser
- Download file 8.07 kB
-
https://huggingface.co/AjayRangoji/AR/resolve/main/ar/data/prepare_data.py
- Command line
-
hf download hf://AjayRangoji/AR/ar/data/prepare_data.py
-
curl -L -o prepare_data.py https://huggingface.co/AjayRangoji/AR/resolve/main/ar/data/prepare_data.py
8.07 kB
| """Offline data preparation (parallel tokenization). | |
| Streams raw text from a Hugging Face dataset, tokenizes it across many CPU | |
| worker processes, and appends the token ids to a per-source .bin file (uint32). | |
| Documents are separated by the tokenizer's end-of-text id. | |
| Code sources are read one language at a time (e.g. "stack_python" reads only | |
| Python) and pass a quality filter that drops data dumps and minified files. | |
| --skip_docs skips documents an earlier run already used, so batches never | |
| overlap. Each run prints the value to pass as the next run's --skip_docs. | |
| """ | |
| import os | |
| # each worker is its own process, so the tokenizer must NOT also spawn internal | |
| # threads — those Rust threads race Python's shutdown and crash on exit | |
| os.environ["TOKENIZERS_PARALLELISM"] = "false" | |
| import argparse | |
| import itertools | |
| import multiprocessing as mp | |
| import re | |
| import numpy as np | |
| from datasets import load_dataset | |
| from .tokenizer import Tokenizer | |
| # stop cleanly (keeping everything written) if the shared disk gets this full | |
| MIN_FREE_BYTES = 300 * 1024**3 | |
| DTYPE = np.uint32 # vocab > 65535, so 2 bytes is not enough | |
| def free_bytes(path: str) -> int: | |
| st = os.statvfs(path) | |
| return st.f_bavail * st.f_frsize | |
| # each source: (HF repo, config, data_dir, text column, split, is_code) | |
| SOURCES = { | |
| "fineweb": ("HuggingFaceFW/fineweb-edu", "sample-350BT", None, "text", "train", False), | |
| "finemath": ("HuggingFaceTB/finemath", "finemath-4plus", None, "text", "train", False), | |
| "finemath3": ("HuggingFaceTB/finemath", "finemath-3plus", None, "text", "train", False), | |
| "cosmopedia": ("HuggingFaceTB/smollm-corpus", "cosmopedia-v2", None, "text", "train", False), | |
| "infiwebmath": ("HuggingFaceTB/finemath", "infiwebmath-4plus", None, "text", "train", False), | |
| "fineweb_hq": ("HuggingFaceFW/fineweb-edu", "sample-350BT", None, "text", "train", False), | |
| "stack": ("bigcode/the-stack-dedup", None, None, "content", "train", True), | |
| } | |
| # one source per language: "stack_python" reads only the data/python folder | |
| CODE_LANGS = ["python", "javascript", "java", "cpp", "typescript", | |
| "go", "rust", "shell", "sql"] | |
| for _lang in CODE_LANGS: | |
| SOURCES[f"stack_{_lang}"] = ("bigcode/the-stack-dedup", None, f"data/{_lang}", | |
| "content", "train", True) | |
| # keep only documents with at least this quality score (fineweb-edu's 0-5 "int_score") | |
| MIN_INT_SCORE = {"fineweb_hq": 4} | |
| # --- code quality filter (StarCoder-style rules) -------------------------- | |
| _HEX = re.compile(r"0x[0-9a-fA-F]{2,}") | |
| def _looks_like_good_code(text: str) -> bool: | |
| lines = text.split("\n") | |
| if max(len(l) for l in lines) > 1000: # minified / one giant line | |
| return False | |
| if len(text) / len(lines) > 100: # very long lines on average | |
| return False | |
| if sum(c.isalnum() for c in text) / len(text) < 0.25: # mostly symbols | |
| return False | |
| if sum(len(m) for m in _HEX.findall(text)) / len(text) > 0.2: # hex data tables | |
| return False | |
| if sum(c.isdigit() for c in text) / len(text) > 0.4: # numeric data tables | |
| return False | |
| return True | |
| # --- worker side --------------------------------------------------------- | |
| # each worker builds its own tokenizer once (tokenizers aren't shareable | |
| # across processes), then reuses it for every document it receives | |
| _worker_tok = None | |
| _worker_is_code = False | |
| def _init_worker(is_code): | |
| global _worker_tok, _worker_is_code | |
| _worker_tok = Tokenizer() | |
| _worker_is_code = is_code | |
| def _encode_one(text: str) -> np.ndarray: | |
| # a rejected document comes back empty; the main process skips it | |
| if not text or (_worker_is_code and not _looks_like_good_code(text)): | |
| return np.empty(0, dtype=DTYPE) | |
| ids = _worker_tok.encode(text) | |
| ids.append(_worker_tok.eot_id) | |
| return np.array(ids, dtype=DTYPE) | |
| # --- main side ----------------------------------------------------------- | |
| SCORE_STATS = {"seen": 0, "low": 0} # docs streamed / docs below min score | |
| def _doc_texts(ds, text_col, min_score=0): | |
| # pull the text out of each document, skipping low-quality ones if asked | |
| for doc in ds: | |
| SCORE_STATS["seen"] += 1 | |
| if min_score and (doc.get("int_score") or 0) < min_score: | |
| SCORE_STATS["low"] += 1 | |
| continue | |
| yield doc[text_col] or "" | |
| def prepare(source: str, out_dir: str, target_tokens: int, | |
| num_workers: int, chunk_docs: int = 10_000, skip_docs: int = 0, | |
| data_files=None): | |
| repo, config, data_dir, text_col, split, is_code = SOURCES[source] | |
| os.makedirs(out_dir, exist_ok=True) | |
| out_path = os.path.join(out_dir, f"{source}.bin") | |
| # fresh data appended to an old file would mix two different runs | |
| if skip_docs == 0 and os.path.exists(out_path) and os.path.getsize(out_path) > 0: | |
| raise SystemExit(f"[refuse] {out_path} already has data: delete it first") | |
| if data_files: | |
| # read one specific file directly (e.g. a late shard for a held-out set) | |
| ds = load_dataset(repo, data_files=data_files, split=split, streaming=True) | |
| else: | |
| ds = load_dataset(repo, config, data_dir=data_dir, split=split, streaming=True) | |
| print(f"[read] {repo} files={data_files} config={config} data_dir={data_dir}", flush=True) | |
| texts = _doc_texts(ds, text_col, MIN_INT_SCORE.get(source, 0)) | |
| # skip documents an earlier run already consumed, so batches never overlap | |
| if skip_docs > 0: | |
| print(f" [skip] skipping first {skip_docs:,} documents", flush=True) | |
| texts = itertools.islice(texts, skip_docs, None) | |
| written = n_docs = n_kept = 0 | |
| stopped_early = False | |
| # imap keeps results IN ORDER and pulls lazily, so memory stays small | |
| pool = mp.Pool(num_workers, initializer=_init_worker, initargs=(is_code,)) | |
| try: | |
| with open(out_path, "ab") as f: | |
| for arr in pool.imap(_encode_one, texts, chunksize=64): | |
| n_docs += 1 # counts every streamed doc (for --skip_docs) | |
| if arr.size: | |
| arr.tofile(f) # main process is the only writer | |
| written += arr.size | |
| n_kept += 1 | |
| if written >= target_tokens: | |
| break | |
| if n_docs % chunk_docs == 0: | |
| if free_bytes(out_dir) < MIN_FREE_BYTES: | |
| print(f"\n[stop] free space below {MIN_FREE_BYTES/1e9:.0f} GB", | |
| flush=True) | |
| stopped_early = True | |
| break | |
| finally: | |
| pool.terminate() | |
| pool.join() | |
| if MIN_INT_SCORE.get(source): | |
| low = 100 * SCORE_STATS["low"] / max(SCORE_STATS["seen"], 1) | |
| print(f"\n[score] {low:.0f}% of streamed docs had int_score < {MIN_INT_SCORE[source]}") | |
| status = "STOPPED EARLY (disk)" if stopped_early else "done" | |
| dropped = 100 * (1 - n_kept / max(n_docs, 1)) | |
| print(f"\n[{status}] {source}: {written/1e6:.1f}M tokens, " | |
| f"{n_docs:,} docs read, {dropped:.0f}% dropped by code filter") | |
| print(f"[next] for the following run, pass --skip_docs {skip_docs + n_docs}", | |
| flush=True) | |
| if __name__ == "__main__": | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--source", required=True, choices=list(SOURCES)) | |
| p.add_argument("--out_dir", default="/fsx/ajay-stu/ar-data/bin") | |
| p.add_argument("--target_tokens", type=int, default=100_000_000) | |
| p.add_argument("--skip_docs", type=int, default=0) | |
| p.add_argument("--num_workers", type=int, default=max(1, mp.cpu_count() - 4)) | |
| p.add_argument("--data_files", default=None) | |
| args = p.parse_args() | |
| prepare(args.source, args.out_dir, args.target_tokens, args.num_workers, | |
| skip_docs=args.skip_docs, data_files=args.data_files or None) | |
| # HF's fast tokenizer leaves native threads that crash normal interpreter | |
| # shutdown; the file is closed above, so exit immediately | |
| os._exit(0) |