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,347 @@
1
+ //! Fast pretokenizer for the o200k_base regex (GPT-4o, gpt-oss):
2
+ //! `[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]*[\p{Ll}\p{Lm}\p{Lo}\p{M}]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]+[\p{Ll}\p{Lm}\p{Lo}\p{M}]*(?i:'s|'t|'re|'ve|'m|'ll|'d)?|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n/]*|\s*[\r\n]+|\s+(?!\S)|\s+`
3
+ //!
4
+ //! See `o200k_family` for the shared scalar walker and mask-scanner
5
+ //! boundary algebra (`CONTRACTIONS = true`, `DIGITS3 = true`).
6
+
7
+ use super::mask::{MaskScheme, MaskState};
8
+ use super::o200k_family;
9
+ use crate::pretokenize::Pretoken;
10
+
11
+ pub(crate) struct O200kScheme;
12
+
13
+ impl MaskScheme for O200kScheme {
14
+ #[inline(always)]
15
+ fn advance(bytes: &[u8], pos: usize) -> usize {
16
+ o200k_family::advance_pos::<true, true, true, false>(bytes, pos)
17
+ }
18
+
19
+ #[cfg(target_arch = "aarch64")]
20
+ #[inline(always)]
21
+ fn batch_masks(bytes: &[u8], scan: usize) -> (u64, u64) {
22
+ o200k_family::batch_masks::<true, true, true, false>(bytes, scan)
23
+ }
24
+
25
+ #[cfg(target_arch = "x86_64")]
26
+ #[inline(always)]
27
+ unsafe fn batch_masks_x86<const AVX512: bool>(bytes: &[u8], scan: usize) -> (u64, u64) {
28
+ // SAFETY: the caller detected the tier (trait contract).
29
+ unsafe { o200k_family::batch_masks_x86::<AVX512, true, true, true, false>(bytes, scan) }
30
+ }
31
+ }
32
+
33
+ /// With SIMD support (aarch64 NEON, or x86_64 AVX-512/AVX2 detected at
34
+ /// runtime), iteration runs the shared o200k-family mask scanner (see
35
+ /// `o200k_family::batch_masks`); elsewhere every token takes the scalar
36
+ /// `advance_pos`.
37
+ pub struct FastO200kPretokenizer<'a> {
38
+ bytes: &'a [u8],
39
+ state: MaskState,
40
+ }
41
+
42
+ impl<'a> FastO200kPretokenizer<'a> {
43
+ #[inline]
44
+ pub fn new(bytes: &'a [u8]) -> Self {
45
+ Self::with_pos(bytes, 0)
46
+ }
47
+
48
+ /// Resume iteration at a byte offset previously returned by [`Self::pos`].
49
+ #[inline]
50
+ pub fn with_pos(bytes: &'a [u8], pos: usize) -> Self {
51
+ Self { bytes, state: MaskState::new(pos) }
52
+ }
53
+
54
+ /// Current position as a byte offset into the input.
55
+ #[inline]
56
+ pub fn pos(&self) -> usize {
57
+ self.state.pos
58
+ }
59
+ }
60
+
61
+ impl<'a> Iterator for FastO200kPretokenizer<'a> {
62
+ type Item = Pretoken<'a>;
63
+
64
+ #[inline]
65
+ fn next(&mut self) -> Option<Pretoken<'a>> {
66
+ let (start, end) = self.state.next_span::<O200kScheme>(self.bytes)?;
67
+ Some(Pretoken(&self.bytes[start..end]))
68
+ }
69
+ }
70
+
71
+ super::impl_mask_pretoken_spans!(FastO200kPretokenizer, O200kScheme);
72
+
73
+ #[cfg(test)]
74
+ pub(crate) mod tests {
75
+ use super::*;
76
+ use std::io::Read;
77
+
78
+ /// The o200k pattern verbatim — no possessive quantifiers, so it runs
79
+ /// directly under fancy-regex.
80
+ const O200K_REF_REGEX: &str = r"[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]*[\p{Ll}\p{Lm}\p{Lo}\p{M}]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]+[\p{Ll}\p{Lm}\p{Lo}\p{M}]*(?i:'s|'t|'re|'ve|'m|'ll|'d)?|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n/]*|\s*[\r\n]+|\s+(?!\S)|\s+";
81
+
82
+ fn regex_tokens(s: &str) -> Vec<String> {
83
+ let re = fancy_regex::Regex::new(O200K_REF_REGEX).unwrap();
84
+ re.find_iter(s)
85
+ .map(|m| m.unwrap().as_str().to_string())
86
+ .collect()
87
+ }
88
+
89
+ fn fast_tokens(s: &str) -> Vec<String> {
90
+ FastO200kPretokenizer::new(s.as_bytes())
91
+ .map(|t| String::from_utf8_lossy(t.0).into_owned())
92
+ .collect()
93
+ }
94
+
95
+ /// Load the first `max_bytes` of ~/data/owt_train.txt, truncated to a
96
+ /// UTF-8 boundary (streamed; the full file is ~12 GB).
97
+ fn load_owt_prefix(max_bytes: usize) -> Vec<u8> {
98
+ let path = std::env::home_dir().unwrap().join("data/owt_train.txt");
99
+ let f = std::fs::File::open(&path).expect("Could not open ~/data/owt_train.txt");
100
+ let mut buf = Vec::with_capacity(max_bytes);
101
+ f.take(max_bytes as u64).read_to_end(&mut buf).unwrap();
102
+ while !buf.is_empty() && std::str::from_utf8(&buf).is_err() {
103
+ buf.pop();
104
+ }
105
+ buf
106
+ }
107
+
108
+ pub(crate) const SMALL_CASES: &[&str] = &[
109
+ "hello",
110
+ "Hello",
111
+ "HELLO",
112
+ "HeLLo",
113
+ "camelCase",
114
+ "PascalCase",
115
+ "HTTPResponse",
116
+ "HTTPresponse",
117
+ "parseHTMLDocument",
118
+ "XMLHttpRequest",
119
+ "aB",
120
+ "aBc",
121
+ "ABc",
122
+ "ABCdef GHIjkl",
123
+ " hello",
124
+ " Hello World",
125
+ "hello world",
126
+ " hello",
127
+ "\thello",
128
+ "\tHello",
129
+ "\nhello",
130
+ "\n\nHello",
131
+ "!hello",
132
+ "!Hello",
133
+ "!!hello",
134
+ "?!x",
135
+ "don't",
136
+ "DON'T",
137
+ "Don'T",
138
+ "don'ts",
139
+ "can'ts more",
140
+ "they'LL go",
141
+ "it'S he'Ll",
142
+ "we'Ve THEY'RE",
143
+ "x'll'd",
144
+ "don't's",
145
+ "o'clock",
146
+ "don'x",
147
+ "x'lm",
148
+ "x'm'm",
149
+ "'sound",
150
+ "'Sound",
151
+ "'lx",
152
+ "'hello",
153
+ " 'hello",
154
+ " 's",
155
+ " 'S",
156
+ "x'0",
157
+ "3's",
158
+ "3'ts",
159
+ "123",
160
+ "1234",
161
+ "1234567",
162
+ " 123",
163
+ " 123",
164
+ "3rd",
165
+ "abc1234def",
166
+ "hello, world!",
167
+ "hi!\n\ndef",
168
+ "hi !!\n\ndef",
169
+ " !!!",
170
+ "a-b",
171
+ "a - b",
172
+ "...",
173
+ "a/b",
174
+ "a//b",
175
+ "http://x.com/path",
176
+ ".\n//x",
177
+ "!\n/",
178
+ "!\n/\n/x",
179
+ "\n/",
180
+ "//\n/",
181
+ "x/\n",
182
+ "x\n/",
183
+ "hello\n",
184
+ "hello \n",
185
+ "hello \nx",
186
+ "hello\n x",
187
+ "hello \n\n ",
188
+ "x \n\n ",
189
+ "x ",
190
+ "x \t",
191
+ " \n hello",
192
+ "\r\nhello",
193
+ "a\r\n",
194
+ "a\r\n ",
195
+ "a\n \n",
196
+ "a \n \t",
197
+ "\n\n",
198
+ "\n\n\t",
199
+ " ",
200
+ " ",
201
+ "",
202
+ "café",
203
+ "Café",
204
+ "CAFÉ",
205
+ "cafÉ",
206
+ " café",
207
+ "\u{a0}word",
208
+ "voilà ¡hola!",
209
+ "ΑΒΓδε",
210
+ "αβΓΔ",
211
+ "Привет Мир",
212
+ "ПРИВЕТ мир",
213
+ "ẞßẞ",
214
+ "Džungla DŽUNGLA džungla",
215
+ "١٢٣٤٥",
216
+ "1٢3x",
217
+ "١٢٣٤٥٦٧",
218
+ "tab\tsep\tvals",
219
+ "\x0bword",
220
+ "a\u{2028}b",
221
+ "a\u{2028}\n",
222
+ "price: $5.99!",
223
+ "'ſ",
224
+ "x'ſ fine",
225
+ "日本語のテキスト",
226
+ " 日本語",
227
+ "日本語ABC",
228
+ "abc日本語Def",
229
+ "e\u{301}f",
230
+ "cafe\u{301} de\u{301}composed",
231
+ "\u{301}leading mark",
232
+ "\u{301}\u{301}two marks",
233
+ " \u{301}abc",
234
+ "\t\u{301}abc",
235
+ "!\u{301}",
236
+ "!\u{301}!",
237
+ "!!\u{301}x",
238
+ "!!\u{301}X",
239
+ "1\u{301}2",
240
+ "'\u{301}s",
241
+ "A\u{301}B",
242
+ "a\u{301}B",
243
+ "x\u{301}'s",
244
+ "деВНАгарІ",
245
+ "देवनागरी में परीक्षण",
246
+ "עִבְרִית נִקּוּד",
247
+ "الْعَرَبِيَّة",
248
+ "a\u{20dd}b",
249
+ " \u{20dd}",
250
+ "\u{200b}\u{301}x",
251
+ ];
252
+
253
+ #[test]
254
+ fn o200k_small_cases() {
255
+ for case in SMALL_CASES {
256
+ assert_eq!(
257
+ fast_tokens(case),
258
+ regex_tokens(case),
259
+ "Mismatch on case {case:?}"
260
+ );
261
+ }
262
+ }
263
+
264
+ /// Random codepoint soup drawn from classes the scheme distinguishes,
265
+ /// compared against the reference regex.
266
+ #[test]
267
+ fn o200k_matches_regex_random() {
268
+ use rand::prelude::*;
269
+ let pools: &[&[char]] = &[
270
+ &['a', 'z', 'é', 'ß', 'ж', 'ا', '한', '日'], // lower/caseless
271
+ &['A', 'Z', 'É', 'Ж', 'DŽ', 'Dž'], // upper/title
272
+ &['1', '9', '٢', '½', 'Ⅷ', '๕'], // numbers
273
+ &[' ', '\t', '\n', '\r', '\u{a0}', '\u{2028}'], // whitespace
274
+ &['\u{301}', '\u{5bf}', '\u{93b}', '\u{20dd}'], // marks
275
+ &['.', ',', '!', '$', '\'', '«', '¡', '€', '☃', '/'], // punct/symbols
276
+ &['\u{0}', '\u{ad}', '\u{200b}', '\u{e0001}'], // other (C*)
277
+ &['s', 't', 'm', 'd', 'l', 'v', 'r', 'e', 'S', 'T', 'L'], // suffix letters
278
+ ];
279
+ let mut rng = StdRng::seed_from_u64(0x93E3_5EED);
280
+ for round in 0..3000 {
281
+ let len = rng.random_range(1..40);
282
+ let s: String = (0..len)
283
+ .map(|_| {
284
+ let pool = pools.choose(&mut rng).unwrap();
285
+ *pool.choose(&mut rng).unwrap()
286
+ })
287
+ .collect();
288
+ assert_eq!(
289
+ fast_tokens(&s),
290
+ regex_tokens(&s),
291
+ "Mismatch on round {round}, case {s:?}"
292
+ );
293
+ }
294
+ }
295
+
296
+ #[test]
297
+ fn o200k_matches_regex_owt() {
298
+ const SIZE: usize = 5_000_000;
299
+ let input = load_owt_prefix(SIZE);
300
+ let text = std::str::from_utf8(&input).unwrap();
301
+ eprintln!(
302
+ "Testing o200k fast pretokenizer vs regex on {:.1} MB of OWT",
303
+ input.len() as f64 / 1e6
304
+ );
305
+
306
+ let re = fancy_regex::Regex::new(O200K_REF_REGEX).unwrap();
307
+ let mut fast_iter = FastO200kPretokenizer::new(&input);
308
+ let mut re_iter = re.find_iter(text);
309
+ let mut token_idx: usize = 0;
310
+ let mut recent: Vec<(String, String)> = Vec::new();
311
+
312
+ loop {
313
+ match (fast_iter.next(), re_iter.next()) {
314
+ (Some(fast_tok), Some(re_match)) => {
315
+ let re_match = re_match.expect("regex match error");
316
+ let fast_str = String::from_utf8_lossy(fast_tok.0);
317
+ let re_str = &text[re_match.start()..re_match.end()];
318
+ recent.push((fast_str.to_string(), re_str.to_string()));
319
+ if recent.len() > 10 {
320
+ recent.remove(0);
321
+ }
322
+ assert_eq!(
323
+ fast_str, re_str,
324
+ "Mismatch at token {token_idx} (byte ~{}).\n fast: {:?}\n regex: {:?}\n recent tokens: {:?}",
325
+ re_match.start(), fast_str, re_str, recent
326
+ );
327
+ }
328
+ (None, None) => break,
329
+ (Some(fast_tok), None) => panic!(
330
+ "Fast produced extra token at index {token_idx}: {:?}\n recent: {:?}",
331
+ String::from_utf8_lossy(fast_tok.0),
332
+ recent
333
+ ),
334
+ (None, Some(re_match)) => {
335
+ let re_match = re_match.expect("regex match error");
336
+ panic!(
337
+ "Regex produced extra token at index {token_idx}: {:?}\n recent: {:?}",
338
+ &text[re_match.start()..re_match.end()],
339
+ recent
340
+ );
341
+ }
342
+ }
343
+ token_idx += 1;
344
+ }
345
+ eprintln!("All {token_idx} tokens match.");
346
+ }
347
+ }