gigatoken 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ use criterion::{criterion_group, criterion_main, Criterion, Throughput};
2
+ #[cfg(all(
3
+ target_arch = "x86_64",
4
+ target_feature = "avx512bw",
5
+ target_feature = "avx512vl"
6
+ ))]
7
+ use gigatoken_rs::pretokenize::reference::avx512::Avx512PretokenizerIter;
8
+ use gigatoken_rs::pretokenize::{
9
+ reference::combinator::pretokens_iterator, FastCl100kPretokenizer, FastQwen2Pretokenizer,
10
+ FastQwen35Pretokenizer, FastR50kPretokenizer, PretokenizerIter,
11
+ };
12
+ use gigatoken_rs::pretokenize::reference::simd::SimdPretokIter;
13
+ use std::hint::black_box;
14
+
15
+ const TARGET_BENCH_SIZE: usize = 100_000_000; // ~100 MB
16
+
17
+ /// Load OWT data, truncated to a UTF-8-safe boundary near `max_bytes`.
18
+ fn load_owt(max_bytes: usize) -> Vec<u8> {
19
+ let data_dir = std::env::home_dir().unwrap().join("data");
20
+ let all_bytes =
21
+ std::fs::read(data_dir.join("owt_train.txt")).expect("Could not read ~/data/owt_train.txt");
22
+ let mut end = max_bytes.min(all_bytes.len());
23
+ // Back up to a UTF-8 character boundary
24
+ while end > 0 && !std::str::from_utf8(&all_bytes[..end]).is_ok() {
25
+ end -= 1;
26
+ }
27
+ all_bytes[..end].to_vec()
28
+ }
29
+
30
+ fn pretokenize_benches(c: &mut Criterion) {
31
+ let input = load_owt(TARGET_BENCH_SIZE);
32
+ let input_len = input.len() as u64;
33
+ eprintln!("Benchmark input size: {:.1} MB", input_len as f64 / 1e6);
34
+
35
+ let mut group = c.benchmark_group("pretokenize");
36
+ group.throughput(Throughput::Bytes(input_len));
37
+ group.sample_size(10);
38
+
39
+ group.bench_function("state_machine", |b| {
40
+ b.iter(|| {
41
+ let count = PretokenizerIter::new(&input).count();
42
+ black_box(count);
43
+ });
44
+ });
45
+
46
+ group.bench_function("winnow", |b| {
47
+ b.iter(|| {
48
+ let mut input_str = unsafe { std::str::from_utf8_unchecked(&input) };
49
+ let count = pretokens_iterator(&mut input_str).count();
50
+ black_box(count);
51
+ });
52
+ });
53
+
54
+ #[cfg(all(
55
+ target_arch = "x86_64",
56
+ target_feature = "avx512bw",
57
+ target_feature = "avx512vl"
58
+ ))]
59
+ group.bench_function("avx512", |b| {
60
+ b.iter(|| {
61
+ let mut iter = Avx512PretokenizerIter::new(&input);
62
+ let mut count = 0;
63
+ while iter.next().is_some() {
64
+ count += 1;
65
+ }
66
+ black_box(count);
67
+ });
68
+ });
69
+ group.bench_function("simd", |b| {
70
+ b.iter(|| {
71
+ let count = SimdPretokIter::new(&input).count();
72
+ black_box(count);
73
+ });
74
+ });
75
+
76
+ group.bench_function("fast_scalar", |b| {
77
+ b.iter(|| {
78
+ let mut iter = FastR50kPretokenizer::new(&input);
79
+ let mut count = 0;
80
+ while iter.next().is_some() {
81
+ count += 1;
82
+ }
83
+ black_box(count);
84
+ });
85
+ });
86
+
87
+ group.bench_function("cl100k_fast_scalar", |b| {
88
+ b.iter(|| {
89
+ let mut iter = FastCl100kPretokenizer::new(&input);
90
+ let mut count = 0;
91
+ while iter.next().is_some() {
92
+ count += 1;
93
+ }
94
+ black_box(count);
95
+ });
96
+ });
97
+
98
+ group.bench_function("qwen2_fast_scalar", |b| {
99
+ b.iter(|| {
100
+ let mut iter = FastQwen2Pretokenizer::new(&input);
101
+ let mut count = 0;
102
+ while iter.next().is_some() {
103
+ count += 1;
104
+ }
105
+ black_box(count);
106
+ });
107
+ });
108
+
109
+ group.bench_function("qwen3_5_fast_scalar", |b| {
110
+ b.iter(|| {
111
+ let mut iter = FastQwen35Pretokenizer::new(&input);
112
+ let mut count = 0;
113
+ while iter.next().is_some() {
114
+ count += 1;
115
+ }
116
+ black_box(count);
117
+ });
118
+ });
119
+
120
+ let re = fancy_regex::Regex::new(
121
+ r"'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+",
122
+ )
123
+ .unwrap();
124
+
125
+ group.bench_function("regex", |b| {
126
+ b.iter(|| {
127
+ let text = unsafe { std::str::from_utf8_unchecked(&input) };
128
+ let count = re.find_iter(text).count();
129
+ black_box(count);
130
+ });
131
+ });
132
+
133
+ // Backtracking-compatible equivalent of the possessive cl100k pattern
134
+ let re_cl100k = fancy_regex::Regex::new(
135
+ r"'(?i:[sdmt]|ll|ve|re)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s+$|\s*[\r\n]|\s+(?!\S)|\s+",
136
+ )
137
+ .unwrap();
138
+
139
+ group.bench_function("cl100k_regex", |b| {
140
+ b.iter(|| {
141
+ let text = unsafe { std::str::from_utf8_unchecked(&input) };
142
+ let count = re_cl100k.find_iter(text).count();
143
+ black_box(count);
144
+ });
145
+ });
146
+
147
+ group.finish();
148
+ }
149
+
150
+ criterion_group!(benches, pretokenize_benches);
151
+ criterion_main!(benches);
@@ -0,0 +1,36 @@
1
+ //! Profiling target for the `FastR50kPretokenizer` hot loop in isolation: a plain
2
+ //! single-pass `main` (no criterion, no BPE encode) that `black_box`es every
3
+ //! yielded pretoken slice, so the slice production can't be optimized away.
4
+
5
+ use gigatoken_rs::pretokenize::FastR50kPretokenizer;
6
+ use std::hint::black_box;
7
+ use std::time::Instant;
8
+
9
+ mod common;
10
+ fn main() {
11
+ let input = common::load_owt_input(None);
12
+ let size_gb = input.len() as f64 / 1e9;
13
+
14
+ // Feed the entire buffer to one pretokenizer in a single pass — this matches
15
+ // the real encode path (`pretokenize_as_iter(text.as_bytes())`), which does
16
+ // not pre-split on newlines.
17
+ let buf: &[u8] = &input;
18
+
19
+ eprintln!("Pretokenizing (fast_scalar, single-threaded, whole buffer)...");
20
+ let start = Instant::now();
21
+ let mut total_tokens: usize = 0;
22
+ // Hand each real pretoken slice to black_box so the bounds computation can't
23
+ // be optimized down to a counter.
24
+ let mut iter = FastR50kPretokenizer::new(buf);
25
+ for pretoken in iter {
26
+ black_box(pretoken);
27
+ total_tokens += 1;
28
+ }
29
+ let elapsed = start.elapsed().as_secs_f64();
30
+ let throughput_gb = size_gb / elapsed;
31
+
32
+ eprintln!(
33
+ "{total_tokens} tokens in {elapsed:.2}s — {throughput_gb:.2} GB/s ({:.0} MB/s)",
34
+ throughput_gb * 1000.0
35
+ );
36
+ }
@@ -0,0 +1,52 @@
1
+ //! Per-scheme variant of `pretokenize_profile`: same single-pass loop over
2
+ //! OWT with every yielded pretoken black_boxed, scheme selected via the
3
+ //! SCHEME env var (r50k | cl100k | olmo3 | qwen2 | qwen3_5 | deepseek_v3).
4
+ //! r50k is also what ByteLevel tokenizers like ModernBERT resolve to. Used
5
+ //! for interleaved A/B runs of the mask-scanner schemes.
6
+
7
+ use gigatoken_rs::pretokenize::{
8
+ FastCl100kPretokenizer, FastDeepSeekV3Pretokenizer, FastOlmo3Pretokenizer,
9
+ FastQwen2Pretokenizer, FastQwen35Pretokenizer, FastR50kPretokenizer,
10
+ };
11
+ use std::hint::black_box;
12
+ use std::time::Instant;
13
+
14
+ mod common;
15
+
16
+ macro_rules! drive {
17
+ ($ty:ty, $buf:expr) => {{
18
+ let mut total_tokens: usize = 0;
19
+ let mut iter = <$ty>::new($buf);
20
+ while let Some(pretoken) = iter.next() {
21
+ black_box(pretoken);
22
+ total_tokens += 1;
23
+ }
24
+ total_tokens
25
+ }};
26
+ }
27
+
28
+ fn main() {
29
+ let input = common::load_owt_input(None);
30
+ let size_gb = input.len() as f64 / 1e9;
31
+ let buf: &[u8] = &input;
32
+ let scheme = std::env::var("SCHEME").unwrap_or_else(|_| "r50k".to_string());
33
+
34
+ eprintln!("Pretokenizing ({scheme}, single-threaded, whole buffer)...");
35
+ let start = Instant::now();
36
+ let total_tokens = match scheme.as_str() {
37
+ "r50k" => drive!(FastR50kPretokenizer, buf),
38
+ "cl100k" => drive!(FastCl100kPretokenizer, buf),
39
+ "olmo3" => drive!(FastOlmo3Pretokenizer, buf),
40
+ "qwen2" => drive!(FastQwen2Pretokenizer, buf),
41
+ "qwen3_5" => drive!(FastQwen35Pretokenizer, buf),
42
+ "deepseek_v3" => drive!(FastDeepSeekV3Pretokenizer, buf),
43
+ other => panic!("unknown SCHEME {other:?}"),
44
+ };
45
+ let elapsed = start.elapsed().as_secs_f64();
46
+ let throughput_gb = size_gb / elapsed;
47
+
48
+ eprintln!(
49
+ "{total_tokens} tokens in {elapsed:.2}s — {throughput_gb:.2} GB/s ({:.0} MB/s)",
50
+ throughput_gb * 1000.0
51
+ );
52
+ }
@@ -0,0 +1,82 @@
1
+ use criterion::{Criterion, Throughput, criterion_group, criterion_main};
2
+ use rayon::prelude::*;
3
+ use std::hint::black_box;
4
+
5
+ const TARGET_BENCH_SIZE: usize = 100_000_000; // ~100 MB
6
+
7
+ /// Load OWT data, truncated to a UTF-8-safe boundary near `max_bytes`.
8
+ fn load_owt(max_bytes: usize) -> Vec<u8> {
9
+ let data_dir = std::env::home_dir().unwrap().join("data");
10
+ let all_bytes =
11
+ std::fs::read(data_dir.join("owt_train.txt")).expect("Could not read ~/data/owt_train.txt");
12
+ let mut end = max_bytes.min(all_bytes.len());
13
+ while end > 0 && std::str::from_utf8(&all_bytes[..end]).is_err() {
14
+ end -= 1;
15
+ }
16
+ all_bytes[..end].to_vec()
17
+ }
18
+
19
+ fn simdutf_transcode_benches(c: &mut Criterion) {
20
+ let input = load_owt(TARGET_BENCH_SIZE);
21
+ let input_len = input.len() as u64;
22
+ eprintln!("Benchmark input size: {:.1} MB", input_len as f64 / 1e6);
23
+
24
+ let mut group = c.benchmark_group("simdutf_transcode");
25
+ group.throughput(Throughput::Bytes(input_len));
26
+ group.sample_size(10);
27
+
28
+ for num_threads in [1, 2, 4, 8, 12, 16] {
29
+ let pool = rayon::ThreadPoolBuilder::new()
30
+ .num_threads(num_threads)
31
+ .build()
32
+ .unwrap();
33
+
34
+ let chunk_size = input.len() / num_threads;
35
+
36
+ // Pre-compute chunk boundaries aligned to UTF-8 char boundaries.
37
+ let mut boundaries = Vec::with_capacity(num_threads + 1);
38
+ boundaries.push(0);
39
+ for i in 1..num_threads {
40
+ let mut pos = chunk_size * i;
41
+ while pos < input.len() && input[pos] & 0b1100_0000 == 0b1000_0000 {
42
+ pos += 1;
43
+ }
44
+ boundaries.push(pos);
45
+ }
46
+ boundaries.push(input.len());
47
+
48
+ // Pre-allocate one destination buffer per thread.
49
+ let mut dst_bufs: Vec<Vec<u32>> = boundaries
50
+ .windows(2)
51
+ .map(|w| vec![0u32; simdutf::utf32_length_from_utf8(&input[w[0]..w[1]])])
52
+ .collect();
53
+
54
+ group.bench_function(format!("utf8_to_utf32_{num_threads}t"), |b| {
55
+ b.iter(|| {
56
+ pool.install(|| {
57
+ let total: usize = boundaries
58
+ .par_windows(2)
59
+ .zip(&mut dst_bufs)
60
+ .map(|(w, dst)| {
61
+ let chunk = &input[w[0]..w[1]];
62
+ let written = unsafe {
63
+ simdutf::convert_valid_utf8_to_utf32(
64
+ chunk.as_ptr(),
65
+ chunk.len(),
66
+ dst.as_mut_ptr(),
67
+ )
68
+ };
69
+ black_box(written)
70
+ })
71
+ .sum();
72
+ black_box(total);
73
+ })
74
+ });
75
+ });
76
+ }
77
+
78
+ group.finish();
79
+ }
80
+
81
+ criterion_group!(benches, simdutf_transcode_benches);
82
+ criterion_main!(benches);
@@ -0,0 +1,88 @@
1
+ use criterion::{Criterion, criterion_group, criterion_main};
2
+ use icu::properties::{CodePointMapDataBorrowed, props::EnumeratedProperty};
3
+ use std::hint::black_box;
4
+
5
+ use rand::{self, Rng, RngExt};
6
+
7
+ // pub fn fibonacci(n: u64) -> u64 {
8
+ // let mut a = 0;
9
+ // let mut b = 1;
10
+ // for _ in 0..n {
11
+ // let c = a + b;
12
+ // a = b;
13
+ // b = c;
14
+ // }
15
+ // a
16
+ // }
17
+
18
+ // Removed dependency since icu is ~95% faster
19
+ // use unicode_properties::{GeneralCategoryGroup, UnicodeGeneralCategory};
20
+ // pub fn unicode_properties_classify(c: char) -> bool {
21
+ // c.general_category_group() == GeneralCategoryGroup::Letter
22
+ // }
23
+
24
+ pub fn icu4x_classify(c: char) -> bool {
25
+ icu::properties::props::GeneralCategoryGroup::Letter
26
+ .contains(icu::properties::props::GeneralCategory::for_char(c))
27
+ }
28
+
29
+ pub fn icu4x_classify_table(c: char) -> bool {
30
+ let gc: CodePointMapDataBorrowed<icu::properties::props::GeneralCategory> =
31
+ icu::properties::CodePointMapData::new();
32
+ icu::properties::props::GeneralCategoryGroup::Letter.contains(gc.get(c))
33
+ }
34
+
35
+ pub fn criterion_benchmark(c: &mut Criterion) {
36
+ // c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
37
+ let mut group = c.benchmark_group("unicode_classify");
38
+
39
+ let chars_input: Vec<char> = rand::rng()
40
+ .sample_iter::<char, _>(rand::distr::StandardUniform)
41
+ .take(4096)
42
+ .collect();
43
+ group.bench_with_input("icu4x", chars_input.as_slice(), |b, chars: &[char]| {
44
+ b.iter(|| {
45
+ for c in chars {
46
+ icu4x_classify(*c);
47
+ }
48
+ });
49
+ });
50
+ // group.bench_with_input(
51
+ // "unicode_properties",
52
+ // chars_input.as_slice(),
53
+ // |b, chars: &[char]| {
54
+ // b.iter(|| {
55
+ // for c in chars {
56
+ // unicode_properties_classify(*c);
57
+ // }
58
+ // });
59
+ // },
60
+ // );
61
+ group.bench_with_input(
62
+ "icu4x table",
63
+ chars_input.as_slice(),
64
+ |b, chars: &[char]| {
65
+ b.iter(|| {
66
+ for c in chars {
67
+ icu4x_classify_table(*c);
68
+ }
69
+ });
70
+ },
71
+ );
72
+
73
+ // c.bench_function("unicode classify letter", |b| {
74
+ // b.iter_batched(
75
+ // || {},
76
+ // |chars: Vec<char>| {
77
+ // for c in chars {
78
+ // unicode_classify(c);
79
+ // }
80
+ // },
81
+ // criterion::BatchSize::SmallInput,
82
+ // )
83
+ // });
84
+ // c.bench_function("unicode pretokenize")
85
+ }
86
+
87
+ criterion_group!(benches, criterion_benchmark);
88
+ criterion_main!(benches);
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "gigatoken-rb"
3
- version = "0.2.2"
3
+ version = "0.4.0"
4
4
  edition = "2021"
5
5
 
6
6
  [lib]
@@ -1,20 +1,40 @@
1
- //! `Gigatoken::Error` is defined in Ruby (`lib/gigatoken.rb`), loaded before
2
- //! this extension is required — looked up fresh at each raise site rather
3
- //! than cached, so no magnus `Value` needs a GC-registered static home.
1
+ //! `Gigatoken::Error` and its subclasses are defined in Ruby
2
+ //! (`lib/gigatoken.rb`), loaded before this extension is required — looked up
3
+ //! fresh at each raise site rather than cached, so no magnus `Value` needs a
4
+ //! GC-registered static home.
4
5
 
5
6
  use magnus::{exception::ExceptionClass, prelude::*, Error, RModule, Ruby};
6
7
 
7
- fn error_class(ruby: &Ruby) -> Result<ExceptionClass, Error> {
8
+ fn error_class(ruby: &Ruby, name: &str) -> Result<ExceptionClass, Error> {
8
9
  ruby.class_object()
9
10
  .const_get::<_, RModule>("Gigatoken")?
10
- .const_get("Error")
11
+ .const_get(name)
11
12
  }
12
13
 
13
- /// Raise `Gigatoken::Error` with `message` (core load/encode failures surface
14
- /// through this — never a Rust panic across the Ruby boundary).
15
- pub fn raise(ruby: &Ruby, message: impl Into<String>) -> Error {
16
- match error_class(ruby) {
14
+ fn raise_as(ruby: &Ruby, name: &str, message: impl Into<String>) -> Error {
15
+ match error_class(ruby, name) {
17
16
  Ok(class) => Error::new(class, message.into()),
18
17
  Err(e) => e,
19
18
  }
20
19
  }
20
+
21
+ /// Raise `Gigatoken::Error` with `message` — what a failure that is neither
22
+ /// the caller's document nor the model itself surfaces through (never a Rust
23
+ /// panic across the Ruby boundary). Prefer [`input_error`] or [`model_error`]
24
+ /// where one of them fits.
25
+ pub fn raise(ruby: &Ruby, message: impl Into<String>) -> Error {
26
+ raise_as(ruby, "Error", message)
27
+ }
28
+
29
+ /// Raise `Gigatoken::InputError`: a document the tokenizer cannot take —
30
+ /// invalid UTF-8 on the SentencePiece path, an id outside the vocabulary in
31
+ /// `decode`.
32
+ pub fn input_error(ruby: &Ruby, message: impl Into<String>) -> Error {
33
+ raise_as(ruby, "InputError", message)
34
+ }
35
+
36
+ /// Raise `Gigatoken::ModelError`: a tokenizer that cannot be loaded — bad or
37
+ /// hostile JSON, an unknown pretokenizer scheme, a malformed `.tiktoken`.
38
+ pub fn model_error(ruby: &Ruby, message: impl Into<String>) -> Error {
39
+ raise_as(ruby, "ModelError", message)
40
+ }