gigatoken 0.1.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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +3016 -0
  3. data/Cargo.toml +135 -0
  4. data/LICENSE +21 -0
  5. data/README.md +141 -0
  6. data/exe/gigatoken +9 -0
  7. data/ext/gigatoken/Cargo.toml +24 -0
  8. data/ext/gigatoken/extconf.rb +19 -0
  9. data/ext/gigatoken/src/error.rs +20 -0
  10. data/ext/gigatoken/src/gvl.rs +122 -0
  11. data/ext/gigatoken/src/lib.rs +50 -0
  12. data/ext/gigatoken/src/sentencepiece.rs +205 -0
  13. data/ext/gigatoken/src/sources.rs +207 -0
  14. data/ext/gigatoken/src/tokenizer.rs +571 -0
  15. data/lib/gigatoken/cli/bench.rb +64 -0
  16. data/lib/gigatoken/cli/support.rb +132 -0
  17. data/lib/gigatoken/cli/validate.rb +55 -0
  18. data/lib/gigatoken/cli.rb +18 -0
  19. data/lib/gigatoken/hub.rb +242 -0
  20. data/lib/gigatoken/packed_result.rb +48 -0
  21. data/lib/gigatoken/tokenizer.rb +117 -0
  22. data/lib/gigatoken/version.rb +5 -0
  23. data/lib/gigatoken.rb +29 -0
  24. data/rust-toolchain.toml +8 -0
  25. data/src/batch.rs +1808 -0
  26. data/src/bindings/bridge.rs +396 -0
  27. data/src/bindings/hub.rs +42 -0
  28. data/src/bindings/matcher.rs +114 -0
  29. data/src/bindings/mod.rs +14 -0
  30. data/src/bindings/padding.rs +177 -0
  31. data/src/bindings/pretokenize.rs +53 -0
  32. data/src/bindings/sources.rs +273 -0
  33. data/src/bindings/train.rs +125 -0
  34. data/src/bpe/mod.rs +1217 -0
  35. data/src/bpe/pretoken_cache.rs +495 -0
  36. data/src/bpe/sentencepiece.rs +1485 -0
  37. data/src/bpe/tiktoken.rs +2555 -0
  38. data/src/bpe_train.rs +351 -0
  39. data/src/input/decompress.rs +11 -0
  40. data/src/input/file_source.rs +514 -0
  41. data/src/input/jsonl.rs +94 -0
  42. data/src/input/mod.rs +333 -0
  43. data/src/input/parquet.rs +303 -0
  44. data/src/lib.rs +578 -0
  45. data/src/load_tokenizer/hf.rs +1036 -0
  46. data/src/load_tokenizer/hub.rs +344 -0
  47. data/src/load_tokenizer/mod.rs +3 -0
  48. data/src/load_tokenizer/tiktoken.rs +87 -0
  49. data/src/main.rs +95 -0
  50. data/src/pretokenize/fast/cl100k.rs +426 -0
  51. data/src/pretokenize/fast/cl100k_family.rs +891 -0
  52. data/src/pretokenize/fast/deepseek_v3.rs +605 -0
  53. data/src/pretokenize/fast/kimi.rs +281 -0
  54. data/src/pretokenize/fast/mask.rs +1486 -0
  55. data/src/pretokenize/fast/mod.rs +446 -0
  56. data/src/pretokenize/fast/nemotron.rs +138 -0
  57. data/src/pretokenize/fast/o200k.rs +347 -0
  58. data/src/pretokenize/fast/o200k_family.rs +1734 -0
  59. data/src/pretokenize/fast/olmo3.rs +505 -0
  60. data/src/pretokenize/fast/qwen2.rs +429 -0
  61. data/src/pretokenize/fast/qwen3_5.rs +541 -0
  62. data/src/pretokenize/fast/r50k.rs +1250 -0
  63. data/src/pretokenize/mod.rs +1079 -0
  64. data/src/pretokenize/options.rs +188 -0
  65. data/src/pretokenize/pretoken.rs +20 -0
  66. data/src/pretokenize/pretokenize_traits.rs +49 -0
  67. data/src/pretokenize/reference/avx512.rs +522 -0
  68. data/src/pretokenize/reference/combinator.rs +572 -0
  69. data/src/pretokenize/reference/mod.rs +28 -0
  70. data/src/pretokenize/reference/simd.rs +852 -0
  71. data/src/pretokenize/reference/state_machine.rs +365 -0
  72. data/src/pretokenize/unicode.rs +546 -0
  73. data/src/test_hub.rs +28 -0
  74. data/src/token.rs +42 -0
  75. metadata +161 -0
@@ -0,0 +1,505 @@
1
+ //! Fast pretokenizer for the Olmo 2/3 (dolma2) regex — on aarch64 (NEON)
2
+ //! and x86_64 with AVX-512 (runtime-detected) a mask scanner via the shared `cl100k_family::batch_masks` boundary algebra,
3
+ //! with the scalar `advance_pos` below as reference, no-SIMD fallback,
4
+ //! and bad-zone/tail executor:
5
+ //! `(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+`
6
+ //!
7
+ //! This is the Qwen2 scheme with cl100k's number rule: `\p{N}{1,3}` matches
8
+ //! runs of up to THREE number chars (Qwen2 matches exactly one). Everything
9
+ //! else — contractions, letter-run prefixes, the `\s*[\r\n]+` newline rule
10
+ //! outranking end-of-input whitespace — is identical to Qwen2.
11
+
12
+ #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
13
+ #[cfg(target_arch = "aarch64")]
14
+ use super::cl100k_family::batch_masks;
15
+ #[cfg(target_arch = "x86_64")]
16
+ use super::cl100k_family::batch_masks_x86;
17
+ use super::mask::{MaskScheme, MaskState};
18
+ use super::{
19
+ decode_cp, is_ascii_ws, is_digit, is_letter, letter_end_at, scan_letters_from,
20
+ scan_newlines, scan_numbers_max3, scan_other_from,
21
+ };
22
+ use crate::pretokenize::Pretoken;
23
+ use crate::pretokenize::unicode::{self, CharClass};
24
+
25
+ pub(crate) struct Olmo3Scheme;
26
+
27
+ impl MaskScheme for Olmo3Scheme {
28
+ #[inline(always)]
29
+ fn advance(bytes: &[u8], pos: usize) -> usize {
30
+ advance_pos(bytes, pos)
31
+ }
32
+
33
+ #[cfg(target_arch = "aarch64")]
34
+ #[inline(always)]
35
+ fn batch_masks(bytes: &[u8], scan: usize) -> (u64, u64) {
36
+ // Class-table LazyLock resolved once per batch; the extended
37
+ // path's per-char classify is then a bare slice index.
38
+ let ct = unicode::ClassTable::get();
39
+ batch_masks(bytes, scan, true, move |cp| ct.class_of(cp))
40
+ }
41
+
42
+ #[cfg(target_arch = "x86_64")]
43
+ #[inline(always)]
44
+ unsafe fn batch_masks_x86<const AVX512: bool>(bytes: &[u8], scan: usize) -> (u64, u64) {
45
+ // Class-table LazyLock resolved once per batch; the extended
46
+ // path's per-char classify is then a bare slice index.
47
+ let ct = unicode::ClassTable::get();
48
+ // SAFETY: the caller detected the tier (trait contract).
49
+ unsafe { batch_masks_x86::<AVX512>(bytes, scan, true, move |cp| ct.class_of(cp)) }
50
+ }
51
+ }
52
+
53
+ /// With SIMD support (aarch64 NEON, or x86_64 AVX-512 detected at runtime),
54
+ /// iteration runs the shared cl100k-family mask scanner (see
55
+ /// `cl100k_family::batch_masks`); elsewhere every token takes the scalar
56
+ /// `advance_pos`.
57
+ pub struct FastOlmo3Pretokenizer<'a> {
58
+ bytes: &'a [u8],
59
+ state: MaskState,
60
+ }
61
+
62
+ impl<'a> FastOlmo3Pretokenizer<'a> {
63
+ #[inline]
64
+ pub fn new(bytes: &'a [u8]) -> Self {
65
+ Self::with_pos(bytes, 0)
66
+ }
67
+
68
+ /// Resume iteration at a byte offset previously returned by [`Self::pos`].
69
+ #[inline]
70
+ pub fn with_pos(bytes: &'a [u8], pos: usize) -> Self {
71
+ Self { bytes, state: MaskState::new(pos) }
72
+ }
73
+
74
+ /// Current position as a byte offset into the input.
75
+ #[inline]
76
+ pub fn pos(&self) -> usize {
77
+ self.state.pos
78
+ }
79
+ }
80
+
81
+ impl<'a> Iterator for FastOlmo3Pretokenizer<'a> {
82
+ type Item = Pretoken<'a>;
83
+
84
+ #[inline]
85
+ fn next(&mut self) -> Option<Pretoken<'a>> {
86
+ let (start, end) = self.state.next_span::<Olmo3Scheme>(self.bytes)?;
87
+ Some(Pretoken(&self.bytes[start..end]))
88
+ }
89
+ }
90
+
91
+ super::impl_mask_pretoken_spans!(FastOlmo3Pretokenizer, Olmo3Scheme);
92
+
93
+ /// Whitespace-led token starting at `start`, i.e. the alternatives
94
+ /// `\s*[\r\n]+` | `\s+(?!\S)` | `\s+`, in that priority.
95
+ /// Precondition: the letter-prefix (`[^\r\n\p{L}\p{N}]?\p{L}+`) and
96
+ /// space+punct (` ?[^\s\p{L}\p{N}]+...`) alternatives were ruled out.
97
+ #[inline(always)]
98
+ fn ws_token_end(bytes: &[u8], start: usize) -> usize {
99
+ let len = bytes.len();
100
+ let mut p = start;
101
+ let mut last_nl_end = 0usize; // 0 = run contains no \r\n
102
+ let mut last_char_start = start;
103
+ while p < len {
104
+ let b = unsafe { *bytes.get_unchecked(p) };
105
+ if b == b'\r' || b == b'\n' {
106
+ last_char_start = p;
107
+ p += 1;
108
+ last_nl_end = p;
109
+ } else if is_ascii_ws(b) {
110
+ last_char_start = p;
111
+ p += 1;
112
+ } else if b >= 0x80 {
113
+ let (cp, l) = unsafe { decode_cp(bytes, p) };
114
+ if unicode::class_of(cp) == CharClass::Whitespace {
115
+ last_char_start = p;
116
+ p += l;
117
+ } else {
118
+ break;
119
+ }
120
+ } else {
121
+ break;
122
+ }
123
+ }
124
+ if last_nl_end != 0 {
125
+ return last_nl_end; // `\s*[\r\n]+`: through the last newline, even at EOS
126
+ }
127
+ if p >= len {
128
+ return p; // `\s+(?!\S)`: lookahead succeeds at EOS
129
+ }
130
+ if last_char_start > start {
131
+ return last_char_start; // `\s+(?!\S)`: all but the last ws char
132
+ }
133
+ p // `\s+`: single whitespace char before content
134
+ }
135
+
136
+ /// Advance past one token starting at `pos`. Returns the new position.
137
+ /// `pos` must be < `bytes.len()`.
138
+ #[inline(always)]
139
+ fn advance_pos(bytes: &[u8], pos: usize) -> usize {
140
+ let b0 = unsafe { *bytes.get_unchecked(pos) };
141
+
142
+ // Hot path 1: ASCII letter — `\p{L}+` with empty prefix
143
+ if is_letter(b0) {
144
+ return scan_letters_from(bytes, pos + 1);
145
+ }
146
+
147
+ // Hot path 2: space prefix
148
+ if b0 == b' ' {
149
+ let Some(&b1) = bytes.get(pos + 1) else {
150
+ return pos + 1; // trailing lone space (`\s+(?!\S)` at EOS)
151
+ };
152
+ if is_letter(b1) {
153
+ return scan_letters_from(bytes, pos + 2); // " word"
154
+ }
155
+ if b1 < 0x80 {
156
+ if is_digit(b1) {
157
+ return pos + 1; // numbers never absorb the space
158
+ }
159
+ if is_ascii_ws(b1) {
160
+ return ws_token_end(bytes, pos);
161
+ }
162
+ // ` ?[^\s\p{L}\p{N}]+[\r\n]*`
163
+ let p = scan_other_from(bytes, pos + 2);
164
+ return scan_newlines(bytes, p);
165
+ }
166
+ let (cp, l) = unsafe { decode_cp(bytes, pos + 1) };
167
+ let p1 = pos + 1 + l;
168
+ match unicode::class_of(cp) {
169
+ CharClass::Letter => return scan_letters_from(bytes, p1),
170
+ CharClass::Whitespace => return ws_token_end(bytes, pos),
171
+ CharClass::Number => return pos + 1,
172
+ CharClass::Other => {
173
+ let p = scan_other_from(bytes, p1);
174
+ return scan_newlines(bytes, p);
175
+ }
176
+ }
177
+ }
178
+
179
+ // Non-ASCII
180
+ if b0 >= 0x80 {
181
+ let (cp, l) = unsafe { decode_cp(bytes, pos) };
182
+ let p0 = pos + l;
183
+ let class = unicode::class_of(cp);
184
+ if class == CharClass::Letter {
185
+ return scan_letters_from(bytes, p0);
186
+ }
187
+ if class == CharClass::Number {
188
+ return scan_numbers_max3(bytes, p0, 1);
189
+ }
190
+ // Any non-letter/number char except \r\n may prefix a letter run
191
+ if let Some(p) = letter_end_at(bytes, p0) {
192
+ return scan_letters_from(bytes, p);
193
+ }
194
+ if class == CharClass::Whitespace {
195
+ return ws_token_end(bytes, pos);
196
+ }
197
+ let p = scan_other_from(bytes, p0);
198
+ return scan_newlines(bytes, p);
199
+ }
200
+
201
+ // ASCII digit: `\p{N}{1,3}`
202
+ if is_digit(b0) {
203
+ return scan_numbers_max3(bytes, pos + 1, 1);
204
+ }
205
+
206
+ // Apostrophe: case-insensitive contractions
207
+ if b0 == b'\'' {
208
+ match bytes.get(pos + 1).map(u8::to_ascii_lowercase) {
209
+ Some(b's' | b'd' | b'm' | b't') => return pos + 2,
210
+ Some(b'l') if bytes.get(pos + 2).map(u8::to_ascii_lowercase) == Some(b'l') => {
211
+ return pos + 3;
212
+ }
213
+ Some(b'v') if bytes.get(pos + 2).map(u8::to_ascii_lowercase) == Some(b'e') => {
214
+ return pos + 3;
215
+ }
216
+ Some(b'r') if bytes.get(pos + 2).map(u8::to_ascii_lowercase) == Some(b'e') => {
217
+ return pos + 3;
218
+ }
219
+ _ => {}
220
+ }
221
+ // U+017F LATIN SMALL LETTER LONG S case-folds to 's' under `(?i)`
222
+ if bytes.get(pos + 1) == Some(&0xC5) && bytes.get(pos + 2) == Some(&0xBF) {
223
+ return pos + 3;
224
+ }
225
+ // Not a contraction: `'` can still prefix a letter run
226
+ if let Some(p) = letter_end_at(bytes, pos + 1) {
227
+ return scan_letters_from(bytes, p);
228
+ }
229
+ let p = scan_other_from(bytes, pos + 1);
230
+ return scan_newlines(bytes, p);
231
+ }
232
+
233
+ // \r and \n are excluded from the letter-run prefix
234
+ if b0 == b'\r' || b0 == b'\n' {
235
+ return ws_token_end(bytes, pos);
236
+ }
237
+
238
+ // Other ASCII whitespace (\t, \x0b, \x0c) may prefix a letter run
239
+ if is_ascii_ws(b0) {
240
+ if let Some(p) = letter_end_at(bytes, pos + 1) {
241
+ return scan_letters_from(bytes, p);
242
+ }
243
+ return ws_token_end(bytes, pos);
244
+ }
245
+
246
+ // ASCII punctuation/symbol
247
+ if let Some(p) = letter_end_at(bytes, pos + 1) {
248
+ return scan_letters_from(bytes, p);
249
+ }
250
+ let p = scan_other_from(bytes, pos + 1);
251
+ scan_newlines(bytes, p)
252
+ }
253
+
254
+ #[cfg(test)]
255
+ mod tests {
256
+ use super::*;
257
+ use std::io::Read;
258
+
259
+ /// The Olmo3/dolma2 pattern verbatim — no possessive quantifiers, so it
260
+ /// runs directly under fancy-regex.
261
+ const OLMO3_REF_REGEX: &str =
262
+ r"(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+";
263
+
264
+ fn regex_tokens(s: &str) -> Vec<String> {
265
+ let re = fancy_regex::Regex::new(OLMO3_REF_REGEX).unwrap();
266
+ re.find_iter(s)
267
+ .map(|m| m.unwrap().as_str().to_string())
268
+ .collect()
269
+ }
270
+
271
+ fn fast_tokens(s: &str) -> Vec<String> {
272
+ FastOlmo3Pretokenizer::new(s.as_bytes())
273
+ .map(|t| String::from_utf8_lossy(t.0).into_owned())
274
+ .collect()
275
+ }
276
+
277
+ /// Load the first `max_bytes` of ~/data/owt_train.txt, truncated to a
278
+ /// UTF-8 boundary (streamed; the full file is ~12 GB).
279
+ fn load_owt_prefix(max_bytes: usize) -> Vec<u8> {
280
+ let path = std::env::home_dir().unwrap().join("data/owt_train.txt");
281
+ let f = std::fs::File::open(&path).expect("Could not open ~/data/owt_train.txt");
282
+ let mut buf = Vec::with_capacity(max_bytes);
283
+ f.take(max_bytes as u64).read_to_end(&mut buf).unwrap();
284
+ while !buf.is_empty() && std::str::from_utf8(&buf).is_err() {
285
+ buf.pop();
286
+ }
287
+ buf
288
+ }
289
+
290
+ #[test]
291
+ fn olmo3_small_cases() {
292
+ let cases = [
293
+ "hello",
294
+ " hello",
295
+ "hello world",
296
+ " hello",
297
+ " hello",
298
+ "\thello",
299
+ "\t\thello",
300
+ "\nhello",
301
+ "\n\nhello",
302
+ "\n\n hello",
303
+ "!hello",
304
+ "!!hello",
305
+ "?!x",
306
+ "don't",
307
+ "DON'T",
308
+ "they'LL go",
309
+ "it'S he'Ll",
310
+ "we'Ve THEY'RE",
311
+ "'sound",
312
+ "'lx",
313
+ "'hello",
314
+ " 'hello",
315
+ " 's",
316
+ "x'0",
317
+ "123",
318
+ "1234",
319
+ "1234567",
320
+ "12345678901",
321
+ " 123",
322
+ " 1234",
323
+ " 123",
324
+ "3rd",
325
+ "abc1234def",
326
+ "3.14159",
327
+ "hello, world!",
328
+ "hi!\n\ndef",
329
+ "hi !!\n\ndef",
330
+ " !!!",
331
+ "a-b",
332
+ "a - b",
333
+ "...",
334
+ "hello\n",
335
+ "hello \n",
336
+ "hello \nx",
337
+ "hello\n x",
338
+ "hello \n\n ",
339
+ "x \n\n ",
340
+ "x ",
341
+ "x \t",
342
+ " \n hello",
343
+ "\r\nhello",
344
+ "a\r\n",
345
+ "a\r\n ",
346
+ "a\n \n",
347
+ "a \n \t",
348
+ "\n\n",
349
+ "\n\n\t",
350
+ " ",
351
+ " ",
352
+ "",
353
+ "café",
354
+ " café",
355
+ "\u{a0}word",
356
+ "voilà ¡hola!",
357
+ "١٢٣٤٥",
358
+ " ١٢٣٤٥",
359
+ "e\u{301}f",
360
+ "日本語のテキスト",
361
+ " 日本語",
362
+ "1٢3x",
363
+ "1٢34",
364
+ "tab\tsep\tvals",
365
+ "\x0bword",
366
+ "a\u{2028}b",
367
+ "a\u{2028}\n",
368
+ "price: $5.99!",
369
+ "'ſ",
370
+ "it'ſ fine",
371
+ ];
372
+ for case in cases {
373
+ assert_eq!(
374
+ fast_tokens(case),
375
+ regex_tokens(case),
376
+ "Mismatch on case {case:?}"
377
+ );
378
+ }
379
+ }
380
+
381
+ #[test]
382
+ fn olmo3_matches_regex_owt() {
383
+ const SIZE: usize = 5_000_000;
384
+ let input = load_owt_prefix(SIZE);
385
+ let text = std::str::from_utf8(&input).unwrap();
386
+ eprintln!(
387
+ "Testing olmo3 fast pretokenizer vs regex on {:.1} MB of OWT",
388
+ input.len() as f64 / 1e6
389
+ );
390
+
391
+ let re = fancy_regex::Regex::new(OLMO3_REF_REGEX).unwrap();
392
+ let mut fast_iter = FastOlmo3Pretokenizer::new(&input);
393
+ let mut re_iter = re.find_iter(text);
394
+ let mut token_idx: usize = 0;
395
+ let mut recent: Vec<(String, String)> = Vec::new();
396
+
397
+ loop {
398
+ match (fast_iter.next(), re_iter.next()) {
399
+ (Some(fast_tok), Some(re_match)) => {
400
+ let re_match = re_match.expect("regex match error");
401
+ let fast_str = String::from_utf8_lossy(fast_tok.0);
402
+ let re_str = &text[re_match.start()..re_match.end()];
403
+ recent.push((fast_str.to_string(), re_str.to_string()));
404
+ if recent.len() > 10 {
405
+ recent.remove(0);
406
+ }
407
+ assert_eq!(
408
+ fast_str, re_str,
409
+ "Mismatch at token {token_idx} (byte ~{}).\n fast: {:?}\n regex: {:?}\n recent tokens: {:?}",
410
+ re_match.start(), fast_str, re_str, recent
411
+ );
412
+ }
413
+ (None, None) => break,
414
+ (Some(fast_tok), None) => panic!(
415
+ "Fast produced extra token at index {token_idx}: {:?}\n recent: {:?}",
416
+ String::from_utf8_lossy(fast_tok.0),
417
+ recent
418
+ ),
419
+ (None, Some(re_match)) => {
420
+ let re_match = re_match.expect("regex match error");
421
+ panic!(
422
+ "Regex produced extra token at index {token_idx}: {:?}\n recent: {:?}",
423
+ &text[re_match.start()..re_match.end()],
424
+ recent
425
+ );
426
+ }
427
+ }
428
+ token_idx += 1;
429
+ }
430
+ eprintln!("All {token_idx} tokens match.");
431
+ }
432
+
433
+ /// Full-OWT (~12 GB) comparison against the reference regex, parallelized
434
+ /// with rayon over ~32 MB chunks cut at newline boundaries. Splitting is
435
+ /// safe because both sides tokenize the identical chunk. Run with:
436
+ /// `cargo test --release olmo3_matches_regex_owt_full -- --ignored --nocapture`
437
+ #[test]
438
+ #[ignore = "reads the full ~12 GB OWT file; run explicitly in release mode"]
439
+ fn olmo3_matches_regex_owt_full() {
440
+ use rayon::prelude::*;
441
+
442
+ let path = std::env::home_dir().unwrap().join("data/owt_train.txt");
443
+ let file = std::fs::File::open(&path).expect("Could not open ~/data/owt_train.txt");
444
+ let mmap = unsafe { memmap2::Mmap::map(&file).unwrap() };
445
+ let bytes: &[u8] = &mmap;
446
+
447
+ const CHUNK: usize = 32 * 1024 * 1024;
448
+ let mut boundaries = vec![0usize];
449
+ while *boundaries.last().unwrap() < bytes.len() {
450
+ let target = (*boundaries.last().unwrap() + CHUNK).min(bytes.len());
451
+ let end = if target == bytes.len() {
452
+ target
453
+ } else {
454
+ // Cut at the next newline (ASCII, so always a UTF-8 boundary).
455
+ match memchr::memchr(b'\n', &bytes[target..]) {
456
+ Some(off) => target + off + 1,
457
+ None => bytes.len(),
458
+ }
459
+ };
460
+ boundaries.push(end);
461
+ }
462
+ eprintln!(
463
+ "Comparing olmo3 fast pretokenizer vs regex on {:.2} GB in {} chunks",
464
+ bytes.len() as f64 / 1e9,
465
+ boundaries.len() - 1
466
+ );
467
+
468
+ let total_tokens: usize = boundaries
469
+ .par_windows(2)
470
+ .map(|w| {
471
+ let chunk = &bytes[w[0]..w[1]];
472
+ let text = std::str::from_utf8(chunk).expect("chunk is not valid UTF-8");
473
+ let re = fancy_regex::Regex::new(OLMO3_REF_REGEX).unwrap();
474
+ let mut fast_iter = FastOlmo3Pretokenizer::new(chunk);
475
+ let mut re_iter = re.find_iter(text);
476
+ let mut count = 0usize;
477
+ loop {
478
+ match (fast_iter.next(), re_iter.next()) {
479
+ (Some(fast_tok), Some(re_match)) => {
480
+ let re_match = re_match.expect("regex match error");
481
+ let fast_str = String::from_utf8_lossy(fast_tok.0);
482
+ let re_str = &text[re_match.start()..re_match.end()];
483
+ assert_eq!(
484
+ fast_str, re_str,
485
+ "Mismatch in chunk at byte {} (chunk offset {})",
486
+ w[0] + re_match.start(),
487
+ re_match.start()
488
+ );
489
+ }
490
+ (None, None) => break,
491
+ (fast, re) => panic!(
492
+ "Token count mismatch in chunk starting at byte {}: fast={:?} regex={:?}",
493
+ w[0],
494
+ fast.map(|t| String::from_utf8_lossy(t.0).into_owned()),
495
+ re.map(|m| m.unwrap().as_str().to_string()),
496
+ ),
497
+ }
498
+ count += 1;
499
+ }
500
+ count
501
+ })
502
+ .sum();
503
+ eprintln!("All {total_tokens} tokens match across the full file.");
504
+ }
505
+ }