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
data/src/bpe_train.rs ADDED
@@ -0,0 +1,351 @@
1
+ use dashmap::DashMap;
2
+ use indicatif::ProgressBar;
3
+ use itertools::Itertools;
4
+ use priority_queue::PriorityQueue;
5
+ use rayon::prelude::*;
6
+ use rustc_hash::FxBuildHasher;
7
+ use std::collections::{BTreeSet, HashMap};
8
+
9
+ use std::hash::Hash;
10
+
11
+ #[derive(Clone)]
12
+ pub struct Word {
13
+ pub symbols: Vec<u32>,
14
+ pub word_count: isize,
15
+ }
16
+
17
+ type Pair = (u32, u32);
18
+
19
+ fn count_pairs(words: &[Word]) -> HashMap<Pair, isize> {
20
+ let mut symbol_counts: HashMap<Pair, isize> = HashMap::new();
21
+ for word in words.iter() {
22
+ for i in 0..word.symbols.len() - 1 {
23
+ let pair = (word.symbols[i], word.symbols[i + 1]);
24
+ let count = symbol_counts.entry(pair).or_insert(0);
25
+ *count += word.word_count;
26
+ }
27
+ }
28
+ symbol_counts
29
+ }
30
+
31
+ fn update_word(
32
+ w: &mut Word,
33
+ pair: Pair,
34
+ new_symbol: u32,
35
+ mut record_changes: impl FnMut((u32, u32), isize),
36
+ ) {
37
+ let mut i = 0;
38
+ while i < w.symbols.len() - 1 {
39
+ if w.symbols[i] == pair.0 && w.symbols[i + 1] == pair.1 {
40
+ // Perform the merge
41
+ if i >= 1 {
42
+ record_changes((w.symbols[i - 1], pair.0), -w.word_count);
43
+ record_changes((w.symbols[i - 1], new_symbol), w.word_count);
44
+ }
45
+ if w.symbols.len() >= 3 && i <= w.symbols.len() - 3 {
46
+ record_changes((pair.1, w.symbols[i + 2]), -w.word_count);
47
+ record_changes((new_symbol, w.symbols[i + 2]), w.word_count);
48
+ }
49
+ w.symbols[i] = new_symbol;
50
+ w.symbols.remove(i + 1);
51
+ }
52
+ i += 1;
53
+ }
54
+ }
55
+
56
+ /// Unsafe hack to parallelize as efficiently as possible.
57
+ /// The borrow checker doesn't allow several mutable references to the same underlying array, so
58
+ /// we need to do unsafe dereferencing.
59
+ /// This is only safe if you there is _only one_ mutable reference to the underlying value.
60
+ #[derive(Clone)]
61
+ struct SendPtr(*mut Word);
62
+
63
+ unsafe impl Sync for SendPtr {}
64
+ unsafe impl Send for SendPtr {}
65
+
66
+ /// Update words by merging the given pair into a new symbol.
67
+ /// Update the contained_in_words map to _add_ associations between the newly created pairs and the words they are contained in (we don't remove old ones, though they will be stale).
68
+ /// Return a map of pair -> change in count (can be negative) to update the priority queue.
69
+ fn update_words(
70
+ words: &mut [Word],
71
+ contained_in_words: &mut HashMap<(u32, u32), BTreeSet<u32>>,
72
+ pair: Pair,
73
+ new_symbol: u32,
74
+ ) -> DashMap<(u32, u32), isize, FxBuildHasher> {
75
+ let count_changes: DashMap<(u32, u32), isize, FxBuildHasher> = DashMap::default();
76
+
77
+ let n_threads = rayon::current_num_threads();
78
+
79
+ // Iterate through all words containing first or second
80
+ let word_idcs = &contained_in_words[&(pair.0, pair.1)];
81
+ let words_ptr = SendPtr(words.as_mut_ptr());
82
+
83
+ // TODO(perf): There is a lot of contention on this map early in merging, since the updated pairs overlap a lot in the beginning.
84
+ // Pair -> Word, pair was added to the word, make sure to update contained_in_words
85
+ let contained_updates: DashMap<(u32, u32), BTreeSet<u32>, FxBuildHasher> = DashMap::default();
86
+
87
+ if word_idcs.len() > 2 * n_threads {
88
+ word_idcs
89
+ .iter()
90
+ .copied()
91
+ .collect::<Vec<_>>()
92
+ .par_chunks(word_idcs.len().div_ceil(n_threads))
93
+ .for_each(|idcs_chunk| {
94
+ for &i in idcs_chunk {
95
+ // Smuggle in a mutable reference to the word
96
+ let local_words_ptr = words_ptr.clone();
97
+ // SAFETY: Only this thread has access to this word, since word_idcs is a set of unique indices.
98
+ let word = unsafe { &mut *local_words_ptr.0.add(i as usize) };
99
+ let count_changes = |pair, change| {
100
+ if change > 0 {
101
+ // Was added to the word, need to track this immediately, since other threads might subtract
102
+ contained_updates.entry(pair).or_default().insert(i);
103
+ }
104
+ *count_changes.entry(pair).or_default() += change;
105
+ };
106
+ update_word(word, pair, new_symbol, count_changes);
107
+ }
108
+ });
109
+ } else {
110
+ // Single-threaded for small updates
111
+ word_idcs.iter().copied().for_each(|i| {
112
+ // Smuggle in a mutable reference to the word
113
+ let local_words_ptr = words_ptr.clone();
114
+ // SAFETY: Only this thread has access to this word, since word_idcs is a set of unique indices.
115
+ let word = unsafe { &mut *local_words_ptr.0.add(i as usize) };
116
+ let count_changes = |pair, change| {
117
+ if change > 0 {
118
+ // Was added to the word, need to track this
119
+ contained_updates.entry(pair).or_default().insert(i);
120
+ }
121
+ *count_changes.entry(pair).or_default() += change;
122
+ };
123
+ update_word(word, pair, new_symbol, count_changes);
124
+ });
125
+ }
126
+
127
+ for (pair, mut word_idcs) in contained_updates.into_iter() {
128
+ let set = contained_in_words.entry(pair).or_default();
129
+ set.append(&mut word_idcs);
130
+ }
131
+
132
+ count_changes
133
+ }
134
+
135
+ pub fn assemble_token(token: u32, symbols: &[Vec<u8>]) -> String {
136
+ symbols[token as usize]
137
+ .iter()
138
+ .map(|x| *x as char)
139
+ .collect::<String>()
140
+ }
141
+
142
+ pub struct BPEResult {
143
+ pub vocab: HashMap<u32, Vec<u8>>,
144
+ pub merges: Vec<(Vec<u8>, Vec<u8>)>,
145
+ }
146
+
147
+ /// How to break ties when multiple pairs have the same frequency count.
148
+ #[derive(Clone, Copy, Debug, Default)]
149
+ pub enum TieBreaking {
150
+ /// Compare by token IDs remapped to match HuggingFace tokenizers' BpeTrainer
151
+ /// initial vocabulary ordering (ByteLevel unicode codepoint order for bytes 0-255).
152
+ #[default]
153
+ HuggingFace,
154
+ /// Compare by raw (u32, u32) token IDs (byte value = token ID).
155
+ RawTokenIds,
156
+ /// Assemble each token's bytes into a string and compare lexicographically.
157
+ AssembledBytes,
158
+ }
159
+
160
+ /// Build a mapping from byte value (0-255) to the rank it would receive in
161
+ /// HuggingFace's BpeTrainer initial vocabulary. HF sorts the ByteLevel alphabet
162
+ /// by unicode codepoint: printable ASCII/Latin-1 bytes keep their codepoint,
163
+ /// while the remaining 68 bytes are remapped to U+0100..U+0143.
164
+ fn build_byte_to_hf_rank() -> [u32; 256] {
165
+ let mut byte_to_cp = [0u32; 256];
166
+ let mut n = 0u32;
167
+ for b in 0..=255u8 {
168
+ let is_allowed = matches!(b, 33..=126 | 161..=172 | 174..=255);
169
+ if is_allowed {
170
+ byte_to_cp[b as usize] = b as u32;
171
+ } else {
172
+ byte_to_cp[b as usize] = 256 + n;
173
+ n += 1;
174
+ }
175
+ }
176
+
177
+ // Sort bytes by their unicode codepoint, then record position as rank
178
+ let mut bytes_sorted: Vec<u8> = (0..=255).collect();
179
+ bytes_sorted.sort_by_key(|&b| byte_to_cp[b as usize]);
180
+
181
+ let mut rank = [0u32; 256];
182
+ for (i, &b) in bytes_sorted.iter().enumerate() {
183
+ rank[b as usize] = i as u32;
184
+ }
185
+ rank
186
+ }
187
+
188
+ pub fn train_bpe<K: AsRef<[u8]> + Eq + Hash>(
189
+ counts: HashMap<K, usize, FxBuildHasher>,
190
+ vocab_size: usize,
191
+ special_tokens: Vec<String>,
192
+ tie_breaking: TieBreaking,
193
+ ) -> BPEResult {
194
+ // Indicates which word indices contain a given symbol
195
+ let mut contained_in_words: HashMap<(u32, u32), BTreeSet<u32>> = HashMap::new();
196
+ let mut contained_in_words_arr = vec![vec![vec![]; 256]; 256];
197
+ let mut words: Vec<Word> = counts
198
+ .into_iter()
199
+ .enumerate()
200
+ .map(|(word_i, (word, count))| {
201
+ // At first we have only bytes, so we won't need to hash the u32 pairs
202
+ let word_symbols: Vec<u32> = word.as_ref().iter().map(|&b| b as u32).collect();
203
+ for c in word_symbols.iter().copied().tuple_windows::<(u32, u32)>() {
204
+ contained_in_words_arr[c.0 as usize][c.1 as usize].push(word_i as u32);
205
+ }
206
+ Word {
207
+ symbols: word_symbols,
208
+ word_count: count as isize,
209
+ }
210
+ })
211
+ .collect();
212
+
213
+ for (i, j) in (0..256).cartesian_product(0..256) {
214
+ if !contained_in_words_arr[i][j].is_empty() {
215
+ contained_in_words.insert(
216
+ (i as u32, j as u32),
217
+ BTreeSet::from_iter(contained_in_words_arr[i][j].iter().copied()),
218
+ );
219
+ }
220
+ }
221
+ drop(contained_in_words_arr);
222
+
223
+ println!("{} unique words", words.len());
224
+ let max_symbols = vocab_size;
225
+
226
+ let symbol_counts = count_pairs(&words);
227
+
228
+ // Symbols 0 through 255 are unicode characters
229
+ let mut symbols: Vec<Vec<u8>> = (0..=255).map(|x| vec![x]).collect();
230
+ symbols.extend(
231
+ special_tokens
232
+ .into_iter()
233
+ .map(|x| x.bytes().collect::<Vec<u8>>()),
234
+ );
235
+
236
+ // Build HF rank table for tie-breaking (only used in HuggingFace mode)
237
+ let hf_rank = build_byte_to_hf_rank();
238
+
239
+ let mut pq = PriorityQueue::new();
240
+ symbol_counts.into_iter().for_each(|(pair, count)| {
241
+ pq.push(pair, count);
242
+ });
243
+
244
+ let mut merges = vec![];
245
+
246
+ println!("Starting merges");
247
+ let bar = ProgressBar::new(max_symbols as u64).with_style(
248
+ indicatif::ProgressStyle::default_bar()
249
+ .template("[{elapsed_precise}] [{bar}] {pos}/{len} ({eta})")
250
+ .unwrap(),
251
+ );
252
+
253
+ while !pq.is_empty() && symbols.len() < max_symbols {
254
+ bar.set_position(symbols.len() as u64);
255
+ let pair = {
256
+ let (first_pair, first_count) = pq.pop().unwrap();
257
+ let mut tied_pairs = vec![first_pair];
258
+ while let Some((_next_pair, &next_count)) = pq.peek() {
259
+ if next_count != first_count {
260
+ break;
261
+ }
262
+ tied_pairs.push(pq.pop().unwrap().0);
263
+ }
264
+ // Find the smallest pair according to the chosen tie-breaking rule
265
+ let mut smallest_pair = first_pair;
266
+ match tie_breaking {
267
+ TieBreaking::HuggingFace => {
268
+ // Remap initial byte token IDs to HF's ByteLevel unicode
269
+ // codepoint ordering before comparison.
270
+ let remap = |id: u32| -> u32 {
271
+ if id < 256 {
272
+ hf_rank[id as usize]
273
+ } else {
274
+ id
275
+ }
276
+ };
277
+ for &pair in &tied_pairs {
278
+ let remapped = (remap(pair.0), remap(pair.1));
279
+ let remapped_smallest =
280
+ (remap(smallest_pair.0), remap(smallest_pair.1));
281
+ if remapped < remapped_smallest {
282
+ smallest_pair = pair;
283
+ }
284
+ }
285
+ }
286
+ TieBreaking::RawTokenIds => {
287
+ for &pair in &tied_pairs {
288
+ if pair < smallest_pair {
289
+ smallest_pair = pair;
290
+ }
291
+ }
292
+ }
293
+ TieBreaking::AssembledBytes => {
294
+ let assemble_pair = |(p0, p1)| {
295
+ (assemble_token(p0, &symbols), assemble_token(p1, &symbols))
296
+ };
297
+ for pair in tied_pairs.iter().copied() {
298
+ if assemble_pair(pair) < assemble_pair(smallest_pair) {
299
+ smallest_pair = pair;
300
+ }
301
+ }
302
+ }
303
+ }
304
+
305
+ for pair in tied_pairs {
306
+ if pair != smallest_pair {
307
+ pq.push(pair, first_count);
308
+ }
309
+ }
310
+
311
+ smallest_pair
312
+ };
313
+
314
+ // Merge the pair
315
+ let new_symbol: Vec<u8> = [&symbols[pair.0 as usize], &symbols[pair.1 as usize]]
316
+ .into_iter()
317
+ .flatten()
318
+ .copied()
319
+ .collect();
320
+
321
+ merges.push((
322
+ symbols[pair.0 as usize].clone(),
323
+ symbols[pair.1 as usize].clone(),
324
+ ));
325
+
326
+ symbols.push(new_symbol);
327
+
328
+ let count_changes = update_words(
329
+ &mut words,
330
+ &mut contained_in_words,
331
+ pair,
332
+ symbols.len() as u32 - 1,
333
+ );
334
+
335
+ for (pair, change) in count_changes.into_iter() {
336
+ let found_item = pq.change_priority_by(&pair, |p| *p += change);
337
+ if !found_item {
338
+ pq.push(pair, change);
339
+ }
340
+ }
341
+ }
342
+ bar.finish();
343
+
344
+ let vocab: HashMap<_, _> = symbols
345
+ .into_iter()
346
+ .enumerate()
347
+ .map(|(i, v)| (i as u32, v))
348
+ .collect();
349
+
350
+ BPEResult { vocab, merges }
351
+ }
@@ -0,0 +1,11 @@
1
+ use std::fs::File;
2
+ use std::io::{self, BufReader};
3
+ use std::path::Path;
4
+
5
+ pub(crate) fn open_gzip(path: &Path) -> io::Result<BufReader<flate2::read::GzDecoder<File>>> {
6
+ Ok(BufReader::new(flate2::read::GzDecoder::new(File::open(path)?)))
7
+ }
8
+
9
+ pub(crate) fn open_zstd(path: &Path) -> io::Result<BufReader<zstd::Decoder<'static, BufReader<File>>>> {
10
+ Ok(BufReader::new(zstd::Decoder::new(File::open(path)?)?))
11
+ }