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,281 @@
1
+ //! Fast pretokenizer for the Kimi regex (moonshotai Kimi-K2 family, from
2
+ //! `tokenization_kimi.py`):
3
+ //! `[\p{Han}]+|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+(?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{Han}]]+[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*(?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+`
4
+ //!
5
+ //! The o200k scheme with a leading `[\p{Han}]+` alternative, Han excluded
6
+ //! from the letter brackets, and no `/` in the absorbed punct tail. See
7
+ //! `o200k_family` (`CONTRACTIONS = true`, `DIGITS3 = true`,
8
+ //! `SLASH = false`, `HAN = true`).
9
+
10
+ use super::mask::{MaskScheme, MaskState};
11
+ use super::o200k_family;
12
+ use crate::pretokenize::Pretoken;
13
+
14
+ pub(crate) struct KimiScheme;
15
+
16
+ impl MaskScheme for KimiScheme {
17
+ #[inline(always)]
18
+ fn advance(bytes: &[u8], pos: usize) -> usize {
19
+ o200k_family::advance_pos::<true, true, false, true>(bytes, pos)
20
+ }
21
+
22
+ #[cfg(target_arch = "aarch64")]
23
+ #[inline(always)]
24
+ fn batch_masks(bytes: &[u8], scan: usize) -> (u64, u64) {
25
+ o200k_family::batch_masks::<true, true, false, true>(bytes, scan)
26
+ }
27
+
28
+ #[cfg(target_arch = "x86_64")]
29
+ #[inline(always)]
30
+ unsafe fn batch_masks_x86<const AVX512: bool>(bytes: &[u8], scan: usize) -> (u64, u64) {
31
+ // SAFETY: the caller detected the tier (trait contract).
32
+ unsafe { o200k_family::batch_masks_x86::<AVX512, true, true, false, true>(bytes, scan) }
33
+ }
34
+ }
35
+
36
+ /// With SIMD support (aarch64 NEON, or x86_64 AVX-512/AVX2 detected at
37
+ /// runtime), iteration runs the shared o200k-family mask scanner (see
38
+ /// `o200k_family::batch_masks`); elsewhere every token takes the scalar
39
+ /// `advance_pos`.
40
+ pub struct FastKimiPretokenizer<'a> {
41
+ bytes: &'a [u8],
42
+ state: MaskState,
43
+ }
44
+
45
+ impl<'a> FastKimiPretokenizer<'a> {
46
+ #[inline]
47
+ pub fn new(bytes: &'a [u8]) -> Self {
48
+ Self::with_pos(bytes, 0)
49
+ }
50
+
51
+ /// Resume iteration at a byte offset previously returned by [`Self::pos`].
52
+ #[inline]
53
+ pub fn with_pos(bytes: &'a [u8], pos: usize) -> Self {
54
+ Self { bytes, state: MaskState::new(pos) }
55
+ }
56
+
57
+ /// Current position as a byte offset into the input.
58
+ #[inline]
59
+ pub fn pos(&self) -> usize {
60
+ self.state.pos
61
+ }
62
+ }
63
+
64
+ impl<'a> Iterator for FastKimiPretokenizer<'a> {
65
+ type Item = Pretoken<'a>;
66
+
67
+ #[inline]
68
+ fn next(&mut self) -> Option<Pretoken<'a>> {
69
+ let (start, end) = self.state.next_span::<KimiScheme>(self.bytes)?;
70
+ Some(Pretoken(&self.bytes[start..end]))
71
+ }
72
+ }
73
+
74
+ super::impl_mask_pretoken_spans!(FastKimiPretokenizer, KimiScheme);
75
+
76
+ #[cfg(test)]
77
+ pub(crate) mod tests {
78
+ use super::*;
79
+ use std::io::Read;
80
+
81
+ /// The Kimi pattern verbatim (from `tokenization_kimi.py`; only greedy
82
+ /// quantifiers, so it runs directly under fancy-regex, which shares
83
+ /// regex-syntax's `&&` class intersection).
84
+ const KIMI_REF_REGEX: &str = r"[\p{Han}]+|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]+(?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{Han}]]+[\p{Ll}\p{Lm}\p{Lo}\p{M}&&[^\p{Han}]]*(?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+";
85
+
86
+ fn regex_tokens(s: &str) -> Vec<String> {
87
+ let re = fancy_regex::Regex::new(KIMI_REF_REGEX).unwrap();
88
+ re.find_iter(s)
89
+ .map(|m| m.unwrap().as_str().to_string())
90
+ .collect()
91
+ }
92
+
93
+ fn fast_tokens(s: &str) -> Vec<String> {
94
+ FastKimiPretokenizer::new(s.as_bytes())
95
+ .map(|t| String::from_utf8_lossy(t.0).into_owned())
96
+ .collect()
97
+ }
98
+
99
+ /// Load the first `max_bytes` of ~/data/owt_train.txt, truncated to a
100
+ /// UTF-8 boundary (streamed; the full file is ~12 GB).
101
+ fn load_owt_prefix(max_bytes: usize) -> Vec<u8> {
102
+ let path = std::env::home_dir().unwrap().join("data/owt_train.txt");
103
+ let f = std::fs::File::open(&path).expect("Could not open ~/data/owt_train.txt");
104
+ let mut buf = Vec::with_capacity(max_bytes);
105
+ f.take(max_bytes as u64).read_to_end(&mut buf).unwrap();
106
+ while !buf.is_empty() && std::str::from_utf8(&buf).is_err() {
107
+ buf.pop();
108
+ }
109
+ buf
110
+ }
111
+
112
+ /// Han-specific cases on top of the shared o200k list: run splits at
113
+ /// script edges, the `[\r\n]*`-only tail (no `/` absorption), Han
114
+ /// numerals inside digit groups, and Han symbols inside punct runs.
115
+ pub(crate) const HAN_CASES: &[&str] = &[
116
+ "中文",
117
+ "中文模型",
118
+ " 中文",
119
+ " 中文",
120
+ "\t中文",
121
+ "\n中文",
122
+ "!中文",
123
+ "中English文",
124
+ "abc中文def",
125
+ "ABC中文",
126
+ "中文ABC",
127
+ "中'se",
128
+ "中's",
129
+ "中'S x",
130
+ "中文's",
131
+ "日本語のテキスト漢字かな",
132
+ "漢字とひらがな",
133
+ "中。文",
134
+ "中,文!",
135
+ "中文123",
136
+ "123中文",
137
+ "1〇2",
138
+ "〇〇",
139
+ "中〇文",
140
+ "1〇〇〇〇",
141
+ "1234〇",
142
+ " 〇",
143
+ "〇1",
144
+ "⼀⼁⼂",
145
+ "!⼀x",
146
+ " ⼀",
147
+ "中⼀文",
148
+ "a⼀b",
149
+ "々中",
150
+ "中々",
151
+ "〆切",
152
+ "㐀㿿",
153
+ "𠀀𠀁",
154
+ "中\u{16FF0}文",
155
+ "!\u{16FF0}",
156
+ ".\n//x",
157
+ "!\n/",
158
+ "!\n/\n/x",
159
+ "\n/",
160
+ "//\n/",
161
+ "x/\n",
162
+ "x\n/",
163
+ "}\n///doc",
164
+ "*/\n/**",
165
+ "`\n//! bindings",
166
+ "path/to/file",
167
+ "http://x.com/path",
168
+ "中\n文",
169
+ "中\r\n文",
170
+ "中 文",
171
+ "中 文",
172
+ "中\n\n文",
173
+ "。\n中",
174
+ "中。\n\n/x",
175
+ ];
176
+
177
+ #[test]
178
+ fn kimi_small_cases() {
179
+ for case in crate::pretokenize::fast::o200k::tests::SMALL_CASES {
180
+ assert_eq!(
181
+ fast_tokens(case),
182
+ regex_tokens(case),
183
+ "Mismatch on case {case:?}"
184
+ );
185
+ }
186
+ for case in HAN_CASES {
187
+ assert_eq!(
188
+ fast_tokens(case),
189
+ regex_tokens(case),
190
+ "Mismatch on case {case:?}"
191
+ );
192
+ }
193
+ }
194
+
195
+ /// Random codepoint soup drawn from classes the scheme distinguishes
196
+ /// (including the Han classes), compared against the reference regex.
197
+ #[test]
198
+ fn kimi_matches_regex_random() {
199
+ use rand::prelude::*;
200
+ let pools: &[&[char]] = &[
201
+ &['a', 'z', 'é', 'ß', 'ж', 'ا', '한', 'ひ', 'カ'], // lower/caseless (non-Han)
202
+ &['A', 'Z', 'É', 'Ж', 'DŽ', 'Dž'], // upper/title
203
+ &['1', '9', '٢', '½', 'Ⅷ', '๕'], // numbers (non-Han)
204
+ &[' ', '\t', '\n', '\r', '\u{a0}', '\u{2028}'], // whitespace
205
+ &['\u{301}', '\u{5bf}', '\u{93b}', '\u{20dd}'], // marks
206
+ &['.', ',', '!', '$', '\'', '«', '¡', '€', '☃', '/'], // punct/symbols
207
+ &['\u{0}', '\u{ad}', '\u{200b}', '\u{e0001}'], // other (C*)
208
+ &['s', 't', 'm', 'd', 'l', 'v', 'r', 'e', 'S', 'T', 'L'], // suffix letters
209
+ &['中', '文', '日', '本', '語', '々', '〆', '㐀', '𠀀'], // Han letters
210
+ &['〇', '〡', '〢', '㆒'], // Han numerals (Nl)
211
+ &['⼀', '⼁', '⺀', '\u{16FF0}'], // Han symbols/marks (So/Mc)
212
+ ];
213
+ let mut rng = StdRng::seed_from_u64(0x93E3_5EEC);
214
+ for round in 0..6000 {
215
+ let len = rng.random_range(1..40);
216
+ let s: String = (0..len)
217
+ .map(|_| {
218
+ let pool = pools.choose(&mut rng).unwrap();
219
+ *pool.choose(&mut rng).unwrap()
220
+ })
221
+ .collect();
222
+ assert_eq!(
223
+ fast_tokens(&s),
224
+ regex_tokens(&s),
225
+ "Mismatch on round {round}, case {s:?}"
226
+ );
227
+ }
228
+ }
229
+
230
+ #[test]
231
+ fn kimi_matches_regex_owt() {
232
+ const SIZE: usize = 5_000_000;
233
+ let input = load_owt_prefix(SIZE);
234
+ let text = std::str::from_utf8(&input).unwrap();
235
+ eprintln!(
236
+ "Testing kimi fast pretokenizer vs regex on {:.1} MB of OWT",
237
+ input.len() as f64 / 1e6
238
+ );
239
+
240
+ let re = fancy_regex::Regex::new(KIMI_REF_REGEX).unwrap();
241
+ let mut fast_iter = FastKimiPretokenizer::new(&input);
242
+ let mut re_iter = re.find_iter(text);
243
+ let mut token_idx: usize = 0;
244
+ let mut recent: Vec<(String, String)> = Vec::new();
245
+
246
+ loop {
247
+ match (fast_iter.next(), re_iter.next()) {
248
+ (Some(fast_tok), Some(re_match)) => {
249
+ let re_match = re_match.expect("regex match error");
250
+ let fast_str = String::from_utf8_lossy(fast_tok.0);
251
+ let re_str = &text[re_match.start()..re_match.end()];
252
+ recent.push((fast_str.to_string(), re_str.to_string()));
253
+ if recent.len() > 10 {
254
+ recent.remove(0);
255
+ }
256
+ assert_eq!(
257
+ fast_str, re_str,
258
+ "Mismatch at token {token_idx} (byte ~{}).\n fast: {:?}\n regex: {:?}\n recent tokens: {:?}",
259
+ re_match.start(), fast_str, re_str, recent
260
+ );
261
+ }
262
+ (None, None) => break,
263
+ (Some(fast_tok), None) => panic!(
264
+ "Fast produced extra token at index {token_idx}: {:?}\n recent: {:?}",
265
+ String::from_utf8_lossy(fast_tok.0),
266
+ recent
267
+ ),
268
+ (None, Some(re_match)) => {
269
+ let re_match = re_match.expect("regex match error");
270
+ panic!(
271
+ "Regex produced extra token at index {token_idx}: {:?}\n recent: {:?}",
272
+ &text[re_match.start()..re_match.end()],
273
+ recent
274
+ );
275
+ }
276
+ }
277
+ token_idx += 1;
278
+ }
279
+ eprintln!("All {token_idx} tokens match.");
280
+ }
281
+ }