#!/usr/bin/env python3 """Stream FineWeb-Edu and write GPT-2 token .bin files for nanoGPT training.""" import argparse import json import os import pickle import shutil import time from typing import Optional import numpy as np import tiktoken from datasets import load_dataset from tqdm import tqdm GPT2_VOCAB_SIZE = 50257 def parse_token_count(value: Optional[str]) -> Optional[int]: if value is None: return None raw = str(value).strip().replace("_", "").lower() if raw in {"none", "all", "full", "0"}: return None multipliers = { "k": 1_000, "m": 1_000_000, "b": 1_000_000_000, } suffix = raw[-1] if suffix in multipliers: return int(float(raw[:-1]) * multipliers[suffix]) return int(raw) def safe_output_paths(out_dir: str, overwrite: bool) -> tuple[str, str, str]: os.makedirs(out_dir, exist_ok=True) train_path = os.path.join(out_dir, "train.bin") val_path = os.path.join(out_dir, "val.bin") meta_path = os.path.join(out_dir, "meta.pkl") existing = [p for p in (train_path, val_path, meta_path) if os.path.exists(p)] if existing and not overwrite: joined = "\n ".join(existing) raise FileExistsError( "Refusing to overwrite existing output files. Remove them or pass " f"--overwrite:\n {joined}" ) return train_path, val_path, meta_path def write_tokens(handle, tokens: list[int]) -> int: if not tokens: return 0 arr = np.asarray(tokens, dtype=np.uint16) arr.tofile(handle) return int(arr.size) def main() -> None: parser = argparse.ArgumentParser( description=( "Stream HuggingFaceFW/fineweb-edu sample-10BT, tokenize with the " "GPT-2 BPE tokenizer, and write data/fineweb/train.bin and val.bin." ) ) parser.add_argument("--out_dir", default="data/fineweb") parser.add_argument("--dataset", default="HuggingFaceFW/fineweb-edu") parser.add_argument("--name", default="sample-10BT") parser.add_argument("--split", default="train") parser.add_argument( "--max_tokens", default=None, help=( "Maximum total tokens to write, e.g. 100M, 1B, 10B. " "Use all/full/0 or omit for the full streamed split." ), ) parser.add_argument( "--val_tokens", default="5M", help="Tokens reserved from the beginning of the stream for training-time val.bin.", ) parser.add_argument( "--min_score", type=float, default=None, help="Optional FineWeb-Edu quality score filter, e.g. 3.0.", ) parser.add_argument("--overwrite", action="store_true") args = parser.parse_args() max_tokens = parse_token_count(args.max_tokens) val_tokens = parse_token_count(args.val_tokens) if val_tokens is None or val_tokens <= 0: raise ValueError("--val_tokens must be a positive token count") if max_tokens is not None and max_tokens <= val_tokens: raise ValueError("--max_tokens must be larger than --val_tokens") train_path, val_path, meta_path = safe_output_paths(args.out_dir, args.overwrite) train_part = train_path + ".part" val_part = val_path + ".part" for path in (train_part, val_part): if os.path.exists(path): if args.overwrite: os.remove(path) else: raise FileExistsError( f"Partial output exists: {path}. Remove it or pass --overwrite." ) enc = tiktoken.get_encoding("gpt2") eot = enc.eot_token ds = load_dataset( args.dataset, name=args.name, split=args.split, streaming=True, ) total_written = 0 val_written = 0 train_written = 0 docs_seen = 0 docs_used = 0 start = time.time() progress_total = max_tokens if max_tokens is not None else None pbar = tqdm(total=progress_total, unit="tok", smoothing=0.05) with open(val_part, "wb") as val_f, open(train_part, "wb") as train_f: for doc in ds: docs_seen += 1 if args.min_score is not None and doc.get("score") is not None: if float(doc["score"]) < args.min_score: continue text = doc.get("text") if not text: continue tokens = [eot] + enc.encode_ordinary(text) if max_tokens is not None: remaining = max_tokens - total_written if remaining <= 0: break tokens = tokens[:remaining] cursor = 0 if val_written < val_tokens: take = min(val_tokens - val_written, len(tokens)) val_written += write_tokens(val_f, tokens[:take]) cursor = take if cursor < len(tokens): train_written += write_tokens(train_f, tokens[cursor:]) docs_used += 1 total_written = val_written + train_written pbar.update(len(tokens)) if max_tokens is not None and total_written >= max_tokens: break pbar.close() os.replace(train_part, train_path) os.replace(val_part, val_path) metadata = { "vocab_size": GPT2_VOCAB_SIZE, "tokenizer": "tiktoken:gpt2", "dataset": args.dataset, "name": args.name, "split": args.split, "max_tokens": max_tokens, "val_tokens_requested": val_tokens, "val_tokens_written": val_written, "train_tokens_written": train_written, "docs_seen": docs_seen, "docs_used": docs_used, "min_score": args.min_score, "elapsed_sec": round(time.time() - start, 2), } with open(meta_path, "wb") as f: pickle.dump({"vocab_size": GPT2_VOCAB_SIZE}, f) with open(os.path.join(args.out_dir, "data_info.json"), "w") as f: json.dump(metadata, f, indent=2) total_size = os.path.getsize(train_path) + os.path.getsize(val_path) print("\nFinished FineWeb-Edu data preparation") print(f" train tokens: {train_written:,} -> {train_path}") print(f" val tokens: {val_written:,} -> {val_path}") print(f" disk size: {total_size / 1024**3:.2f} GiB") print(f" meta: {meta_path}") print(f" info: {os.path.join(args.out_dir, 'data_info.json')}") if shutil.disk_usage(args.out_dir).free < 5 * 1024**3: print("WARNING: less than 5 GiB free space remains in the output directory.") if __name__ == "__main__": main()