unitoken 0.1.4

Fast BPE tokenizer/trainer with a Rust core and Python bindings
Documentation
use std::{
  alloc::{GlobalAlloc, Layout, System},
  hint::black_box,
  path::PathBuf,
  sync::atomic::{AtomicU64, Ordering},
  time::{Duration, Instant},
};

use unitoken::{
  bpe::{encoder::BpeBuilder, Idx},
  pretokenizer::{split_special_tokens, SplitChunk},
  spec::gpt2::Gpt2Spec,
  traits::Encode as _,
};

const R50K_PAT: &str =
  r"'(?:[sdmt]|ll|ve|re)| ?\p{L}++| ?\p{N}++| ?[^\s\p{L}\p{N}]++|\s++$|\s+(?!\S)|\s";
const DEFAULT_CHARS: usize = 200_000;
const DEFAULT_REPEATS: usize = 100;
const FIXTURE: &str = "tinystories_sample_5M";

struct CountingAllocator;

static ALLOC_CALLS: AtomicU64 = AtomicU64::new(0);
static ALLOC_BYTES: AtomicU64 = AtomicU64::new(0);

#[global_allocator]
static GLOBAL: CountingAllocator = CountingAllocator;

unsafe impl GlobalAlloc for CountingAllocator {
  unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
    ALLOC_CALLS.fetch_add(1, Ordering::Relaxed);
    ALLOC_BYTES.fetch_add(layout.size() as u64, Ordering::Relaxed);
    unsafe { System.alloc(layout) }
  }

  unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
    unsafe { System.dealloc(ptr, layout) }
  }
}

#[derive(Clone, Copy)]
struct Sample {
  duration: Duration,
  alloc_calls: u64,
  alloc_bytes: u64,
}

fn reset_allocs() {
  ALLOC_CALLS.store(0, Ordering::Relaxed);
  ALLOC_BYTES.store(0, Ordering::Relaxed);
}

fn sample<F>(mut f: F) -> Sample
where
  F: FnMut() -> usize,
{
  reset_allocs();
  let started = Instant::now();
  let value = f();
  let duration = started.elapsed();
  black_box(value);
  Sample {
    duration,
    alloc_calls: ALLOC_CALLS.load(Ordering::Relaxed),
    alloc_bytes: ALLOC_BYTES.load(Ordering::Relaxed),
  }
}

fn measure<F>(label: &str, repeats: usize, mut f: F)
where
  F: FnMut() -> usize,
{
  let mut samples = Vec::with_capacity(repeats);
  for _ in 0..repeats {
    samples.push(sample(&mut f));
  }
  samples.sort_by_key(|s| s.duration);
  let median = samples[samples.len() / 2];
  let min = samples[0];
  println!(
    "{label:32} min_ms={:.4} median_ms={:.4} alloc_calls={} alloc_bytes={}",
    min.duration.as_secs_f64() * 1000.0,
    median.duration.as_secs_f64() * 1000.0,
    median.alloc_calls,
    median.alloc_bytes,
  );
}

fn prefix_by_chars(text: String, chars: usize) -> String {
  text.chars().take(chars).collect()
}

fn parse_arg(args: &mut impl Iterator<Item = String>, default: usize, name: &str) -> usize {
  args
    .next()
    .as_deref()
    .map_or(Ok(default), str::parse::<usize>)
    .unwrap_or_else(|_| panic!("{name} must be an integer"))
}

fn main() {
  let mut args = std::env::args().skip(1);
  let chars = parse_arg(&mut args, DEFAULT_CHARS, "chars");
  let repeats = parse_arg(&mut args, DEFAULT_REPEATS, "repeats");
  assert!(repeats > 0, "repeats must be greater than zero");

  let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
  let text = prefix_by_chars(
    std::fs::read_to_string(root.join("fixtures").join(format!("{FIXTURE}.txt"))).unwrap(),
    chars,
  );
  let bpe = BpeBuilder::new()
    .load_merges_file(root.join("fixtures").join(format!("merges.{FIXTURE}.txt")), &Gpt2Spec)
    .unwrap()
    .load_vocab_file(root.join("fixtures").join(format!("vocab.{FIXTURE}.json")), &Gpt2Spec)
    .unwrap()
    .set_pat_str(Some(R50K_PAT.to_string()))
    .special_tokens(vec!["<|endoftext|>".to_string()])
    .build(&Gpt2Spec)
    .unwrap();

  let encoded: Vec<Idx> = bpe.encode_string(&text).unwrap();
  println!("chars={} bytes={} repeats={} tokens={}", text.chars().count(), text.len(), repeats, encoded.len());

  measure("split_special_tokens", repeats, || {
    let parts = split_special_tokens(&text, &bpe.pre_tokenizer.re_special_tokens).unwrap();
    parts.len()
  });
  measure("regex_tokens_after_split", repeats, || {
    let parts = split_special_tokens(&text, &bpe.pre_tokenizer.re_special_tokens).unwrap();
    let mut count = 0;
    for part in parts {
      match part {
        SplitChunk::Special(_) => count += 1,
        SplitChunk::Chunk(chunk) => {
          for token in bpe.pre_tokenizer.re_pat.find_iter(chunk) {
            black_box(token.unwrap().as_str());
            count += 1;
          }
        }
      }
    }
    count
  });
  measure("encode_string", repeats, || bpe.encode_string(&text).unwrap().len());
  measure("decode", repeats, || bpe.decode(&encoded).unwrap().len());
}