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 statistics
import sys
import time
from collections.abc import Callable, Sequence
from pathlib import Path
from typing import Any, cast

from uni_tokenizer import BoundaryMode
from uni_tokenizer import PreTokenizer

from common import DEFAULT_CHUNK_SIZE
from common import SPECIAL_TOKENS
from common import add_report_args
from common import benchmark_metadata
from common import resolve_report_path
from common import write_report


SCRIPT_NAME = "profile_pretokenizer"


def time_call(fn: Callable[[], Any], repeats: int) -> tuple[Any, dict[str, float]]:
  samples = []
  result = None
  for _ in range(repeats):
    started = time.perf_counter()
    result = fn()
    samples.append(time.perf_counter() - started)
  return result, {
    "min_s": min(samples),
    "median_s": statistics.median(samples),
    "mean_s": statistics.mean(samples),
  }


def summarize_words(words: dict[str, int]) -> dict[str, int]:
  return {
    "unique_words": len(words),
    "occurrences": sum(words.values()),
  }


def profile(args: argparse.Namespace) -> dict[str, Any]:
  pretokenizer = PreTokenizer(SPECIAL_TOKENS, SPECIAL_TOKENS[0])
  file_size = args.input.stat().st_size
  boundary = cast(BoundaryMode, args.boundary)

  boundaries, boundary_timing = time_call(
    lambda: pretokenizer.find_chunk_boundaries(
      args.input,
      chunk_size=args.chunk_size,
      boundary=boundary,
    ),
    args.repeats,
  )

  first_segments = boundaries[:args.segments]
  segment_results = []
  for offset, length in first_segments:
    words, timing = time_call(
      lambda offset=offset, length=length: pretokenizer.get_words_from_segment(args.input, offset, length),
      args.repeats,
    )
    segment_results.append({
      "offset": offset,
      "length": length,
      **timing,
      **summarize_words(words),
    })

  full_words, full_timing = time_call(
    lambda: pretokenizer.get_words_from_file(
      args.input,
      chunk_size=args.chunk_size,
      boundary=boundary,
    ),
    args.repeats,
  )

  return {
    "metadata": benchmark_metadata(
      contract="raw_text_unitoken_pretokenizer_profile",
      script_name=SCRIPT_NAME,
      dataset_name=args.dataset_name,
      config_name=args.config_name,
      experiment_name=args.experiment_name,
      notes=[
        "Profiles unitoken pretokenizer chunk boundary and word inventory phases.",
      ],
    ),
    "source": {
      "input_kind": "raw_text",
      "input": str(args.input),
      "bytes": file_size,
      "chunk_size": args.chunk_size,
      "boundary": boundary,
    },
    "repeats": args.repeats,
    "boundary_count": len(boundaries),
    "find_chunk_boundaries": boundary_timing,
    "segments": segment_results,
    "get_words_from_file": {
      **full_timing,
      **summarize_words(full_words),
    },
  }


def main(argv: Sequence[str] | None = None) -> int:
  parser = argparse.ArgumentParser(description="Profile unitoken pretokenizer phases on a raw text file.")
  parser.add_argument("input", type=Path)
  parser.add_argument("--chunk-size", type=int, default=DEFAULT_CHUNK_SIZE)
  parser.add_argument("--boundary", choices=["auto", "eot", "line", "utf8"], default="auto")
  parser.add_argument("--segments", type=int, default=4)
  parser.add_argument("--repeats", type=int, default=1)
  add_report_args(parser)
  args = parser.parse_args(argv)

  if args.chunk_size < 1:
    parser.error("--chunk-size must be at least 1")
  if args.segments < 0:
    parser.error("--segments must be non-negative")
  if args.repeats < 1:
    parser.error("--repeats must be at least 1")

  result = profile(args)
  rendered = json.dumps(result, indent=2)
  if not args.quiet:
    print(rendered)
  write_report(resolve_report_path(args, script_name=SCRIPT_NAME), rendered)
  return 0


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