unitoken 0.1.5

Fast BPE tokenizer/trainer with a Rust core and Python bindings
Documentation
from __future__ import annotations

import argparse
import json
import sys
import time
from collections.abc import Sequence
from pathlib import Path
from typing import Any

import pyarrow.parquet as pq


DEFAULT_OUTPUT = Path("out") / "data" / "fineweb2" / "fineweb2_1GiB.txt"
DEFAULT_SEPARATOR = "<|endoftext|>"
GIB = 1024 ** 3


def write_document(out, text: str) -> int:
  encoded = text.encode("utf-8")
  out.write(encoded)
  return len(encoded)


def parquet_files(input_dir: Path, limit: int | None) -> list[Path]:
  files = sorted(input_dir.glob("*.parquet"))
  if limit is not None:
    files = files[:limit]
  if not files:
    raise FileNotFoundError(f"no parquet files found in {input_dir}")
  return files


def create_sample(args: argparse.Namespace) -> dict[str, Any]:
  files = parquet_files(args.input_dir, args.max_files)
  args.output.parent.mkdir(parents=True, exist_ok=True)

  started = time.perf_counter()
  bytes_written = 0
  docs_written = 0
  files_read = 0
  separator = args.separator.encode("utf-8")

  with args.output.open("wb") as out:
    for path in files:
      if docs_written > 0 and bytes_written >= args.size_bytes:
        break

      files_read += 1
      parquet = pq.ParquetFile(path)
      for batch in parquet.iter_batches(batch_size=args.batch_size, columns=[args.column]):
        if docs_written > 0 and bytes_written >= args.size_bytes:
          break
        for scalar in batch.column(0):
          if docs_written > 0 and bytes_written >= args.size_bytes:
            break
          text = scalar.as_py()
          if text is None:
            continue

          if docs_written > 0:
            out.write(separator)
            bytes_written += len(separator)

          bytes_written += write_document(out, text)
          docs_written += 1
        if docs_written > 0 and bytes_written >= args.size_bytes:
          break

  if bytes_written < args.size_bytes and args.pad:
    with args.output.open("ab") as out:
      padding = args.size_bytes - bytes_written
      out.write(b" " * padding)
      bytes_written += padding

  return {
    "input_dir": str(args.input_dir),
    "output": str(args.output),
    "size_bytes": args.size_bytes,
    "bytes_written": bytes_written,
    "docs_written": docs_written,
    "files_read": files_read,
    "separator": args.separator,
    "elapsed_s": time.perf_counter() - started,
  }


def main(argv: Sequence[str] | None = None) -> int:
  parser = argparse.ArgumentParser(description="Create a raw UTF-8 FineWeb2 text sample from local Parquet shards.")
  parser.add_argument("--input-dir", type=Path, required=True)
  parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
  parser.add_argument("--size-bytes", type=int, default=GIB)
  parser.add_argument("--column", default="text")
  parser.add_argument("--batch-size", type=int, default=1024)
  parser.add_argument("--max-files", type=int)
  parser.add_argument("--separator", default=DEFAULT_SEPARATOR, help="String inserted between complete documents.")
  parser.add_argument("--pad", action="store_true", help="Pad with spaces if the source data is smaller than requested.")
  parser.add_argument("--json", type=Path)
  args = parser.parse_args(argv)

  if args.size_bytes < 1:
    parser.error("--size-bytes must be at least 1")
  if args.batch_size < 1:
    parser.error("--batch-size must be at least 1")

  result = create_sample(args)
  rendered = json.dumps(result, indent=2)
  print(rendered)
  if args.json:
    args.json.parent.mkdir(parents=True, exist_ok=True)
    args.json.write_text(rendered + "\n", encoding="utf-8")
  return 0 if result["bytes_written"] >= args.size_bytes else 1


if __name__ == "__main__":
  raise SystemExit(main(sys.argv[1:]))