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,852 @@
1
+ //! SIMD-accelerated pretokenizer.
2
+ //!
3
+ //! Implements the GPT-2 regex:
4
+ //! '(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+
5
+ //!
6
+ //! Key optimizations:
7
+ //! - Inlined fast path for space+letter and letter patterns (~75% of pretokens)
8
+ //! - Table-based byte classification for remaining cases (no cascading if/else)
9
+ //! - Specialized NEON scanners for each character class
10
+ //! - Direct byte-level iteration (no &str conversion, no winnow overhead)
11
+
12
+ use crate::pretokenize::Pretoken;
13
+ use crate::pretokenize::unicode;
14
+
15
+ // ---- Character class constants ----
16
+ // CO and CA are adjacent (0,1) so "other" = class < 2
17
+ const CO: u8 = 0; // Other
18
+ const CA: u8 = 1; // Apostrophe
19
+ const CL: u8 = 2; // Letter
20
+ const CN: u8 = 3; // Number
21
+ const CS: u8 = 4; // Space
22
+ const CW: u8 = 5; // Whitespace control
23
+ const CH: u8 = 7; // Non-ASCII
24
+
25
+ const fn build_class_table() -> [u8; 128] {
26
+ let mut t = [CO; 128];
27
+ let mut i = 0;
28
+ while i < 26 {
29
+ t[b'A' as usize + i] = CL;
30
+ t[b'a' as usize + i] = CL;
31
+ i += 1;
32
+ }
33
+ i = 0;
34
+ while i < 10 {
35
+ t[b'0' as usize + i] = CN;
36
+ i += 1;
37
+ }
38
+ t[b' ' as usize] = CS;
39
+ t[9] = CW;
40
+ t[10] = CW;
41
+ t[11] = CW;
42
+ t[12] = CW;
43
+ t[13] = CW;
44
+ t[b'\'' as usize] = CA;
45
+ t
46
+ }
47
+
48
+ static ASCII_CLASS: [u8; 128] = build_class_table();
49
+
50
+ #[inline(always)]
51
+ fn classify_byte(b: u8) -> u8 {
52
+ if b < 0x80 {
53
+ unsafe { *ASCII_CLASS.get_unchecked(b as usize) }
54
+ } else {
55
+ CH
56
+ }
57
+ }
58
+
59
+ /// Check if byte is ASCII letter using branchless arithmetic.
60
+ #[inline(always)]
61
+ fn is_ascii_letter(b: u8) -> bool {
62
+ (b | 0x20).wrapping_sub(b'a') < 26
63
+ }
64
+
65
+ #[inline(always)]
66
+ unsafe fn decode_char(bytes: &[u8]) -> char {
67
+ unsafe {
68
+ std::str::from_utf8_unchecked(bytes)
69
+ .chars()
70
+ .next()
71
+ .unwrap_unchecked()
72
+ }
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // NEON module
77
+ // ---------------------------------------------------------------------------
78
+
79
+ #[cfg(target_arch = "aarch64")]
80
+ mod neon {
81
+ use std::arch::aarch64::*;
82
+
83
+ #[inline(always)]
84
+ pub unsafe fn first_nonzero_lane(mask: uint8x16_t) -> usize {
85
+ unsafe {
86
+ let lo = vgetq_lane_u64::<0>(vreinterpretq_u64_u8(mask));
87
+ if lo != 0 {
88
+ return (lo.trailing_zeros() as usize) / 8;
89
+ }
90
+ let hi = vgetq_lane_u64::<1>(vreinterpretq_u64_u8(mask));
91
+ if hi != 0 {
92
+ return 8 + (hi.trailing_zeros() as usize) / 8;
93
+ }
94
+ 16
95
+ }
96
+ }
97
+
98
+ #[inline(always)]
99
+ unsafe fn is_letter_mask(chunk: uint8x16_t) -> uint8x16_t {
100
+ unsafe {
101
+ let lower = vorrq_u8(chunk, vdupq_n_u8(0x20));
102
+ let sub = vsubq_u8(lower, vdupq_n_u8(b'a'));
103
+ vcgtq_u8(vdupq_n_u8(26), sub)
104
+ }
105
+ }
106
+
107
+ #[inline(always)]
108
+ unsafe fn is_digit_mask(chunk: uint8x16_t) -> uint8x16_t {
109
+ unsafe {
110
+ let sub = vsubq_u8(chunk, vdupq_n_u8(b'0'));
111
+ vcgtq_u8(vdupq_n_u8(10), sub)
112
+ }
113
+ }
114
+
115
+ #[inline(always)]
116
+ unsafe fn is_other_mask(chunk: uint8x16_t) -> uint8x16_t {
117
+ unsafe {
118
+ let is_letter = is_letter_mask(chunk);
119
+ let is_digit = is_digit_mask(chunk);
120
+ let ws_sub = vsubq_u8(chunk, vdupq_n_u8(9));
121
+ let is_ws_ctrl = vcgeq_u8(vdupq_n_u8(4), ws_sub);
122
+ let is_space = vceqq_u8(chunk, vdupq_n_u8(b' '));
123
+ let is_ws = vorrq_u8(is_ws_ctrl, is_space);
124
+ let is_high = vcgeq_u8(chunk, vdupq_n_u8(0x80));
125
+ let any_exclude =
126
+ vorrq_u8(vorrq_u8(is_letter, is_digit), vorrq_u8(is_ws, is_high));
127
+ vmvnq_u8(any_exclude)
128
+ }
129
+ }
130
+
131
+ /// Scan for end of ASCII letter run. Returns position of first non-letter.
132
+ #[inline(always)]
133
+ pub unsafe fn scan_letters(bytes: &[u8], start: usize) -> usize {
134
+ unsafe {
135
+ let mut i = start;
136
+ while i + 16 <= bytes.len() {
137
+ let chunk = vld1q_u8(bytes.as_ptr().add(i));
138
+ let m = is_letter_mask(chunk);
139
+ let nm = vmvnq_u8(m);
140
+ let pos = first_nonzero_lane(nm);
141
+ if pos < 16 {
142
+ return i + pos;
143
+ }
144
+ i += 16;
145
+ }
146
+ i
147
+ }
148
+ }
149
+
150
+ #[inline(always)]
151
+ pub unsafe fn scan_digits(bytes: &[u8], start: usize) -> usize {
152
+ unsafe {
153
+ let mut i = start;
154
+ while i + 16 <= bytes.len() {
155
+ let chunk = vld1q_u8(bytes.as_ptr().add(i));
156
+ let m = is_digit_mask(chunk);
157
+ let nm = vmvnq_u8(m);
158
+ let pos = first_nonzero_lane(nm);
159
+ if pos < 16 {
160
+ return i + pos;
161
+ }
162
+ i += 16;
163
+ }
164
+ i
165
+ }
166
+ }
167
+
168
+ #[inline(always)]
169
+ pub unsafe fn scan_other(bytes: &[u8], start: usize) -> usize {
170
+ unsafe {
171
+ let mut i = start;
172
+ while i + 16 <= bytes.len() {
173
+ let chunk = vld1q_u8(bytes.as_ptr().add(i));
174
+ let m = is_other_mask(chunk);
175
+ let nm = vmvnq_u8(m);
176
+ let pos = first_nonzero_lane(nm);
177
+ if pos < 16 {
178
+ return i + pos;
179
+ }
180
+ i += 16;
181
+ }
182
+ i
183
+ }
184
+ }
185
+ }
186
+
187
+ #[cfg(not(target_arch = "aarch64"))]
188
+ mod neon {
189
+ #[inline(always)]
190
+ pub unsafe fn scan_letters(_bytes: &[u8], start: usize) -> usize {
191
+ start
192
+ }
193
+ #[inline(always)]
194
+ pub unsafe fn scan_digits(_bytes: &[u8], start: usize) -> usize {
195
+ start
196
+ }
197
+ #[inline(always)]
198
+ pub unsafe fn scan_other(_bytes: &[u8], start: usize) -> usize {
199
+ start
200
+ }
201
+ }
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Run scanners with scalar tail + unicode continuation
205
+ // ---------------------------------------------------------------------------
206
+
207
+ /// SWAR (u64 arithmetic) letter detection: returns bitmask with high bit set
208
+ /// in each byte lane that is NOT an ASCII letter. Returns 0 if all 8 are letters.
209
+ #[inline(always)]
210
+ fn swar_non_letter_mask(word: u64) -> u64 {
211
+ const HI: u64 = 0x8080_8080_8080_8080;
212
+ // Lowercase everything, then check if in ['a','z']
213
+ let lowered = word | 0x2020_2020_2020_2020;
214
+ let ge_a = (lowered | HI).wrapping_sub(0x6161_6161_6161_6161) & HI;
215
+ let le_z = (0x7A7A_7A7A_7A7A_7A7A | HI).wrapping_sub(lowered) & HI;
216
+ // NOT letter = high bit not set in both checks
217
+ !(ge_a & le_z) & HI
218
+ }
219
+
220
+ #[inline(always)]
221
+ fn scan_letter_run(bytes: &[u8], from: usize) -> usize {
222
+ let mut i = from;
223
+
224
+ // SWAR: check 8 bytes at a time with u64 arithmetic (fast for short words)
225
+ while i + 8 <= bytes.len() {
226
+ let word = unsafe { std::ptr::read_unaligned(bytes.as_ptr().add(i) as *const u64) };
227
+ if word & 0x8080_8080_8080_8080 != 0 {
228
+ return scan_letter_run_nonascii(bytes, i);
229
+ }
230
+ let not_letter = swar_non_letter_mask(word);
231
+ if not_letter != 0 {
232
+ return i + (not_letter.trailing_zeros() as usize) / 8;
233
+ }
234
+ i += 8;
235
+ }
236
+
237
+ // Padded SWAR for tail (0-7 remaining bytes)
238
+ let remaining = bytes.len() - i;
239
+ if remaining > 0 {
240
+ let mut padded: u64 = 0; // zero bytes classify as non-letter
241
+ unsafe {
242
+ std::ptr::copy_nonoverlapping(
243
+ bytes.as_ptr().add(i),
244
+ &mut padded as *mut u64 as *mut u8,
245
+ remaining,
246
+ );
247
+ }
248
+ if padded & 0x8080_8080_8080_8080 != 0 {
249
+ return scan_letter_run_nonascii(bytes, i);
250
+ }
251
+ let not_letter = swar_non_letter_mask(padded);
252
+ if not_letter != 0 {
253
+ return i + ((not_letter.trailing_zeros() as usize) / 8).min(remaining);
254
+ }
255
+ return i + remaining;
256
+ }
257
+ i
258
+ }
259
+
260
+ /// Non-ASCII encountered during letter scan. Handle byte-by-byte with unicode.
261
+ #[cold]
262
+ #[inline(never)]
263
+ fn scan_letter_run_nonascii(bytes: &[u8], from: usize) -> usize {
264
+ let mut i = from;
265
+ while i < bytes.len() {
266
+ let b = unsafe { *bytes.get_unchecked(i) };
267
+ if is_ascii_letter(b) {
268
+ i += 1;
269
+ } else if b >= 0x80 {
270
+ let c = unsafe { decode_char(bytes.get_unchecked(i..)) };
271
+ if unicode::is_letter(c) {
272
+ i += c.len_utf8();
273
+ // Re-enter SWAR for subsequent ASCII
274
+ return scan_letter_run(bytes, i);
275
+ }
276
+ break;
277
+ } else {
278
+ break;
279
+ }
280
+ }
281
+ i
282
+ }
283
+
284
+ /// SWAR digit detection: high bit set in each non-digit byte lane.
285
+ #[inline(always)]
286
+ fn swar_non_digit_mask(word: u64) -> u64 {
287
+ const HI: u64 = 0x8080_8080_8080_8080;
288
+ let ge_0 = (word | HI).wrapping_sub(0x3030_3030_3030_3030) & HI;
289
+ let le_9 = (0x3939_3939_3939_3939 | HI).wrapping_sub(word) & HI;
290
+ !(ge_0 & le_9) & HI
291
+ }
292
+
293
+ #[inline(always)]
294
+ fn scan_digit_run(bytes: &[u8], from: usize) -> usize {
295
+ let mut i = from;
296
+
297
+ while i + 8 <= bytes.len() {
298
+ let word = unsafe { std::ptr::read_unaligned(bytes.as_ptr().add(i) as *const u64) };
299
+ if word & 0x8080_8080_8080_8080 != 0 {
300
+ break;
301
+ }
302
+ let not_digit = swar_non_digit_mask(word);
303
+ if not_digit != 0 {
304
+ return i + (not_digit.trailing_zeros() as usize) / 8;
305
+ }
306
+ i += 8;
307
+ }
308
+
309
+ if i + 16 <= bytes.len() && unsafe { *bytes.get_unchecked(i) } < 0x80 {
310
+ i = unsafe { neon::scan_digits(bytes, i) };
311
+ }
312
+
313
+ while i < bytes.len() {
314
+ let b = unsafe { *bytes.get_unchecked(i) };
315
+ if b < 0x80 {
316
+ if classify_byte(b) == CN {
317
+ i += 1;
318
+ } else {
319
+ break;
320
+ }
321
+ } else {
322
+ let c = unsafe { decode_char(bytes.get_unchecked(i..)) };
323
+ if unicode::is_number(c) {
324
+ i += c.len_utf8();
325
+ continue;
326
+ }
327
+ break;
328
+ }
329
+ }
330
+ i
331
+ }
332
+
333
+ /// SWAR "other" detection: high bit set in each byte that IS a letter, digit,
334
+ /// whitespace, or non-ASCII (i.e., NOT "other").
335
+ #[inline(always)]
336
+ fn swar_non_other_mask(word: u64) -> u64 {
337
+ const HI: u64 = 0x8080_8080_8080_8080;
338
+ // "other" = ASCII && NOT letter && NOT digit && NOT whitespace
339
+ let lowered = word | 0x2020_2020_2020_2020;
340
+ let ge_a = (lowered | HI).wrapping_sub(0x6161_6161_6161_6161) & HI;
341
+ let le_z = (0x7A7A_7A7A_7A7A_7A7A | HI).wrapping_sub(lowered) & HI;
342
+ let is_letter = ge_a & le_z & HI;
343
+
344
+ let ge_0 = (word | HI).wrapping_sub(0x3030_3030_3030_3030) & HI;
345
+ let le_9 = (0x3939_3939_3939_3939 | HI).wrapping_sub(word) & HI;
346
+ let is_digit = ge_0 & le_9 & HI;
347
+
348
+ // Whitespace: byte == 0x20 OR byte in [9,13]
349
+ // For SWAR, check: (b - 9) <= 4 OR b == 0x20
350
+ let ge_9 = (word | HI).wrapping_sub(0x0909_0909_0909_0909) & HI;
351
+ let le_13 = (0x0D0D_0D0D_0D0D_0D0D | HI).wrapping_sub(word) & HI;
352
+ let is_ws_ctrl = ge_9 & le_13 & HI;
353
+ // Space: each byte XOR 0x20, check zero
354
+ // For each byte: if byte == 0x20, (byte ^ 0x20) == 0
355
+ // Detect zero bytes using the standard SWAR trick
356
+ let xor_space = word ^ 0x2020_2020_2020_2020;
357
+ let has_zero = (xor_space.wrapping_sub(0x0101_0101_0101_0101)) & !xor_space & HI;
358
+ let is_space = has_zero;
359
+ let is_ws = is_ws_ctrl | is_space;
360
+
361
+ // "not other" = is_letter | is_digit | is_ws
362
+ is_letter | is_digit | is_ws
363
+ }
364
+
365
+ #[inline(always)]
366
+ fn scan_other_run(bytes: &[u8], from: usize) -> usize {
367
+ let mut i = from;
368
+
369
+ while i + 8 <= bytes.len() {
370
+ let word = unsafe { std::ptr::read_unaligned(bytes.as_ptr().add(i) as *const u64) };
371
+ if word & 0x8080_8080_8080_8080 != 0 {
372
+ break;
373
+ }
374
+ let not_other = swar_non_other_mask(word);
375
+ if not_other != 0 {
376
+ return i + (not_other.trailing_zeros() as usize) / 8;
377
+ }
378
+ i += 8;
379
+ }
380
+
381
+ if i + 16 <= bytes.len() && i < bytes.len() && unsafe { *bytes.get_unchecked(i) } < 0x80 {
382
+ i = unsafe { neon::scan_other(bytes, i) };
383
+ }
384
+
385
+ while i < bytes.len() {
386
+ let b = unsafe { *bytes.get_unchecked(i) };
387
+ if b < 0x80 {
388
+ let cl = classify_byte(b);
389
+ if cl < 2 {
390
+ i += 1;
391
+ } else {
392
+ break;
393
+ }
394
+ } else {
395
+ let c = unsafe { decode_char(bytes.get_unchecked(i..)) };
396
+ if unicode::is_other_complete(c) {
397
+ i += c.len_utf8();
398
+ continue;
399
+ }
400
+ break;
401
+ }
402
+ }
403
+ i
404
+ }
405
+
406
+ // ---------------------------------------------------------------------------
407
+ // Tight counting loop (avoids Iterator/Option/Pretoken overhead)
408
+ // ---------------------------------------------------------------------------
409
+
410
+ /// Count pretokens without constructing them. Same logic as next() but no
411
+ /// Option/Pretoken wrapping, yielding a tighter loop.
412
+ fn count_pretokens(bytes: &[u8], mut pos: usize) -> usize {
413
+ let mut count = 0usize;
414
+ while pos < bytes.len() {
415
+ let first = unsafe { *bytes.get_unchecked(pos) };
416
+
417
+ // Fast path: space + letter
418
+ if first == b' ' {
419
+ if pos + 1 < bytes.len() {
420
+ let second = unsafe { *bytes.get_unchecked(pos + 1) };
421
+ if is_ascii_letter(second) {
422
+ pos = scan_letter_run(bytes, pos + 2);
423
+ count += 1;
424
+ continue;
425
+ }
426
+ let class = classify_byte(second);
427
+ pos = match class {
428
+ CN => scan_digit_run(bytes, pos + 2),
429
+ c if c < 2 => scan_other_run(bytes, pos + 2),
430
+ CS | CW => {
431
+ count += count_ws_pretokens(bytes, &mut pos);
432
+ continue;
433
+ }
434
+ _ => {
435
+ // CH — non-ASCII after space
436
+ let c = unsafe { decode_char(bytes.get_unchecked(pos + 1..)) };
437
+ let clen = c.len_utf8();
438
+ if unicode::is_letter(c) {
439
+ scan_letter_run(bytes, pos + 1 + clen)
440
+ } else if unicode::is_number(c) {
441
+ scan_digit_run(bytes, pos + 1 + clen)
442
+ } else if unicode::is_whitespace(c) {
443
+ count += count_ws_pretokens(bytes, &mut pos);
444
+ continue;
445
+ } else {
446
+ scan_other_run(bytes, pos + 1 + clen)
447
+ }
448
+ }
449
+ };
450
+ count += 1;
451
+ continue;
452
+ }
453
+ pos += 1;
454
+ count += 1;
455
+ continue;
456
+ }
457
+
458
+ // Fast path: letter
459
+ if is_ascii_letter(first) {
460
+ pos = scan_letter_run(bytes, pos + 1);
461
+ count += 1;
462
+ continue;
463
+ }
464
+
465
+ // General dispatch
466
+ let class = classify_byte(first);
467
+ match class {
468
+ CN => {
469
+ pos = scan_digit_run(bytes, pos + 1);
470
+ }
471
+ CO => {
472
+ pos = scan_other_run(bytes, pos + 1);
473
+ }
474
+ CW => {
475
+ count += count_ws_pretokens(bytes, &mut pos);
476
+ continue;
477
+ }
478
+ CA => {
479
+ pos = match bytes.get(pos + 1) {
480
+ Some(b's' | b'd' | b'm' | b't') => pos + 2,
481
+ Some(b'l') if bytes.get(pos + 2) == Some(&b'l') => pos + 3,
482
+ Some(b'v') if bytes.get(pos + 2) == Some(&b'e') => pos + 3,
483
+ Some(b'r') if bytes.get(pos + 2) == Some(&b'e') => pos + 3,
484
+ _ => scan_other_run(bytes, pos + 1),
485
+ };
486
+ }
487
+ _ => {
488
+ // CH — non-ASCII
489
+ let c = unsafe { decode_char(bytes.get_unchecked(pos..)) };
490
+ let clen = c.len_utf8();
491
+ if unicode::is_letter(c) {
492
+ pos = scan_letter_run(bytes, pos + clen);
493
+ } else if unicode::is_number(c) {
494
+ pos = scan_digit_run(bytes, pos + clen);
495
+ } else if unicode::is_whitespace(c) {
496
+ count += count_ws_pretokens(bytes, &mut pos);
497
+ continue;
498
+ } else {
499
+ pos = scan_other_run(bytes, pos + clen);
500
+ }
501
+ }
502
+ }
503
+ count += 1;
504
+ }
505
+ count
506
+ }
507
+
508
+ /// Count the pretokens emitted from a whitespace run starting at *pos,
509
+ /// advancing *pos past all the whitespace.
510
+ #[inline]
511
+ fn count_ws_pretokens(bytes: &[u8], pos: &mut usize) -> usize {
512
+ let start = *pos;
513
+ let mut i = start;
514
+
515
+ // Scan all consecutive whitespace
516
+ while i < bytes.len() {
517
+ let b = unsafe { *bytes.get_unchecked(i) };
518
+ if b < 0x80 {
519
+ let cl = classify_byte(b);
520
+ if cl == CS || cl == CW {
521
+ i += 1;
522
+ } else {
523
+ break;
524
+ }
525
+ } else {
526
+ let c = unsafe { decode_char(bytes.get_unchecked(i..)) };
527
+ if unicode::is_whitespace(c) {
528
+ i += c.len_utf8();
529
+ } else {
530
+ break;
531
+ }
532
+ }
533
+ }
534
+
535
+ if i >= bytes.len() {
536
+ // Whitespace at end of input: each char is a separate pretoken
537
+ let mut count = 0;
538
+ let mut j = start;
539
+ while j < i {
540
+ let b = unsafe { *bytes.get_unchecked(j) };
541
+ if b >= 0x80 {
542
+ j += unsafe { decode_char(bytes.get_unchecked(j..)) }.len_utf8();
543
+ } else {
544
+ j += 1;
545
+ }
546
+ count += 1;
547
+ }
548
+ *pos = i;
549
+ return count;
550
+ }
551
+
552
+ // Find start of last ws char
553
+ let mut last_ws_start = i - 1;
554
+ while last_ws_start > start && unsafe { *bytes.get_unchecked(last_ws_start) } & 0xC0 == 0x80
555
+ {
556
+ last_ws_start -= 1;
557
+ }
558
+
559
+ if last_ws_start == start {
560
+ // Single ws char
561
+ let b = unsafe { *bytes.get_unchecked(start) };
562
+ *pos = if b >= 0x80 {
563
+ start + unsafe { decode_char(bytes.get_unchecked(start..)) }.len_utf8()
564
+ } else {
565
+ start + 1
566
+ };
567
+ return 1;
568
+ }
569
+
570
+ // 2+ ws chars: "all but last" is 1 pretoken, last ws char re-enters dispatch
571
+ *pos = last_ws_start;
572
+ 1
573
+ }
574
+
575
+ // ---------------------------------------------------------------------------
576
+ // SimdPretokIter
577
+ // ---------------------------------------------------------------------------
578
+
579
+ pub struct SimdPretokIter<'a> {
580
+ bytes: &'a [u8],
581
+ pos: usize,
582
+ }
583
+
584
+ impl<'a> SimdPretokIter<'a> {
585
+ #[inline]
586
+ pub fn new(bytes: &'a [u8]) -> Self {
587
+ Self { bytes, pos: 0 }
588
+ }
589
+ }
590
+
591
+ impl<'a> Iterator for SimdPretokIter<'a> {
592
+ type Item = Pretoken<'a>;
593
+
594
+ #[inline(always)]
595
+ fn next(&mut self) -> Option<Self::Item> {
596
+ let bytes = self.bytes;
597
+ let start = self.pos;
598
+ if start >= bytes.len() {
599
+ return None;
600
+ }
601
+
602
+ let first = unsafe { *bytes.get_unchecked(start) };
603
+
604
+ // ---- FAST PATH: space + ASCII letter (most common pattern) ----
605
+ if first == b' ' {
606
+ if start + 1 < bytes.len() {
607
+ let second = unsafe { *bytes.get_unchecked(start + 1) };
608
+ if is_ascii_letter(second) {
609
+ let end = scan_letter_run(bytes, start + 2);
610
+ self.pos = end;
611
+ return Some(Pretoken(unsafe { bytes.get_unchecked(start..end) }));
612
+ }
613
+ // Space + non-letter: use table dispatch on second byte
614
+ return self.handle_space_nonletter(start, second);
615
+ }
616
+ // Space at end of input
617
+ self.pos = start + 1;
618
+ return Some(Pretoken(unsafe {
619
+ bytes.get_unchecked(start..start + 1)
620
+ }));
621
+ }
622
+
623
+ // ---- FAST PATH: ASCII letter (standalone word) ----
624
+ if is_ascii_letter(first) {
625
+ let end = scan_letter_run(bytes, start + 1);
626
+ self.pos = end;
627
+ return Some(Pretoken(unsafe { bytes.get_unchecked(start..end) }));
628
+ }
629
+
630
+ // ---- REMAINING CASES: digit, other, whitespace, apostrophe, non-ASCII ----
631
+ let class = classify_byte(first);
632
+ match class {
633
+ CN => {
634
+ let end = scan_digit_run(bytes, start + 1);
635
+ self.pos = end;
636
+ Some(Pretoken(unsafe { bytes.get_unchecked(start..end) }))
637
+ }
638
+ CO => {
639
+ let end = scan_other_run(bytes, start + 1);
640
+ self.pos = end;
641
+ Some(Pretoken(unsafe { bytes.get_unchecked(start..end) }))
642
+ }
643
+ CW => self.handle_whitespace(start),
644
+ CA => self.handle_apostrophe(start),
645
+ _ => self.handle_non_ascii(start), // CH
646
+ }
647
+ }
648
+ }
649
+
650
+ impl<'a> SimdPretokIter<'a> {
651
+ /// Handle space followed by non-letter byte.
652
+ #[inline]
653
+ fn handle_space_nonletter(&mut self, start: usize, second: u8) -> Option<Pretoken<'a>> {
654
+ let bytes = self.bytes;
655
+ let class = classify_byte(second);
656
+ let end = match class {
657
+ CN => scan_digit_run(bytes, start + 2),
658
+ CO | CA => scan_other_run(bytes, start + 2),
659
+ CS | CW => return self.handle_whitespace(start),
660
+ _ => {
661
+ // CH — non-ASCII after space
662
+ let c = unsafe { decode_char(bytes.get_unchecked(start + 1..)) };
663
+ let clen = c.len_utf8();
664
+ if unicode::is_letter(c) {
665
+ scan_letter_run(bytes, start + 1 + clen)
666
+ } else if unicode::is_number(c) {
667
+ scan_digit_run(bytes, start + 1 + clen)
668
+ } else if unicode::is_whitespace(c) {
669
+ return self.handle_whitespace(start);
670
+ } else {
671
+ scan_other_run(bytes, start + 1 + clen)
672
+ }
673
+ }
674
+ };
675
+ self.pos = end;
676
+ Some(Pretoken(unsafe { bytes.get_unchecked(start..end) }))
677
+ }
678
+
679
+ #[cold]
680
+ #[inline(never)]
681
+ fn handle_whitespace(&mut self, start: usize) -> Option<Pretoken<'a>> {
682
+ let bytes = self.bytes;
683
+ let mut i = start;
684
+
685
+ while i < bytes.len() {
686
+ let b = unsafe { *bytes.get_unchecked(i) };
687
+ if b < 0x80 {
688
+ let cl = classify_byte(b);
689
+ if cl == CS || cl == CW {
690
+ i += 1;
691
+ } else {
692
+ break;
693
+ }
694
+ } else {
695
+ let c = unsafe { decode_char(bytes.get_unchecked(i..)) };
696
+ if unicode::is_whitespace(c) {
697
+ i += c.len_utf8();
698
+ } else {
699
+ break;
700
+ }
701
+ }
702
+ }
703
+
704
+ if i >= bytes.len() {
705
+ // End of input: emit single ws char
706
+ let b = unsafe { *bytes.get_unchecked(start) };
707
+ self.pos = if b >= 0x80 {
708
+ start + unsafe { decode_char(bytes.get_unchecked(start..)) }.len_utf8()
709
+ } else {
710
+ start + 1
711
+ };
712
+ return Some(Pretoken(unsafe {
713
+ bytes.get_unchecked(start..self.pos)
714
+ }));
715
+ }
716
+
717
+ // Find start of last ws char
718
+ let mut last_ws_start = i - 1;
719
+ while last_ws_start > start
720
+ && unsafe { *bytes.get_unchecked(last_ws_start) } & 0xC0 == 0x80
721
+ {
722
+ last_ws_start -= 1;
723
+ }
724
+
725
+ if last_ws_start == start {
726
+ // Only 1 ws char
727
+ let b = unsafe { *bytes.get_unchecked(start) };
728
+ self.pos = if b >= 0x80 {
729
+ start + unsafe { decode_char(bytes.get_unchecked(start..)) }.len_utf8()
730
+ } else {
731
+ start + 1
732
+ };
733
+ return Some(Pretoken(unsafe {
734
+ bytes.get_unchecked(start..self.pos)
735
+ }));
736
+ }
737
+
738
+ // 2+ ws chars: emit all but last
739
+ self.pos = last_ws_start;
740
+ Some(Pretoken(unsafe {
741
+ bytes.get_unchecked(start..last_ws_start)
742
+ }))
743
+ }
744
+
745
+ #[cold]
746
+ #[inline(never)]
747
+ fn handle_apostrophe(&mut self, start: usize) -> Option<Pretoken<'a>> {
748
+ let bytes = self.bytes;
749
+ match bytes.get(start + 1) {
750
+ Some(b's' | b'd' | b'm' | b't') => {
751
+ self.pos = start + 2;
752
+ Some(Pretoken(unsafe { bytes.get_unchecked(start..start + 2) }))
753
+ }
754
+ Some(b'l') if bytes.get(start + 2) == Some(&b'l') => {
755
+ self.pos = start + 3;
756
+ Some(Pretoken(unsafe { bytes.get_unchecked(start..start + 3) }))
757
+ }
758
+ Some(b'v') if bytes.get(start + 2) == Some(&b'e') => {
759
+ self.pos = start + 3;
760
+ Some(Pretoken(unsafe { bytes.get_unchecked(start..start + 3) }))
761
+ }
762
+ Some(b'r') if bytes.get(start + 2) == Some(&b'e') => {
763
+ self.pos = start + 3;
764
+ Some(Pretoken(unsafe { bytes.get_unchecked(start..start + 3) }))
765
+ }
766
+ _ => {
767
+ let end = scan_other_run(bytes, start + 1);
768
+ self.pos = end;
769
+ Some(Pretoken(unsafe { bytes.get_unchecked(start..end) }))
770
+ }
771
+ }
772
+ }
773
+
774
+ #[cold]
775
+ #[inline(never)]
776
+ fn handle_non_ascii(&mut self, start: usize) -> Option<Pretoken<'a>> {
777
+ let bytes = self.bytes;
778
+ let c = unsafe { decode_char(bytes.get_unchecked(start..)) };
779
+ let clen = c.len_utf8();
780
+
781
+ let end = if unicode::is_letter(c) {
782
+ scan_letter_run(bytes, start + clen)
783
+ } else if unicode::is_number(c) {
784
+ scan_digit_run(bytes, start + clen)
785
+ } else if unicode::is_whitespace(c) {
786
+ return self.handle_whitespace(start);
787
+ } else {
788
+ scan_other_run(bytes, start + clen)
789
+ };
790
+
791
+ self.pos = end;
792
+ Some(Pretoken(unsafe { bytes.get_unchecked(start..end) }))
793
+ }
794
+ }
795
+
796
+ #[cfg(test)]
797
+ mod tests {
798
+ use super::*;
799
+ use itertools::Itertools;
800
+ use std::cmp::min;
801
+
802
+ #[test]
803
+ fn count_matches_next() {
804
+ let data_dir = std::env::home_dir().unwrap().join("data");
805
+ let input =
806
+ std::fs::read_to_string(data_dir.join("TinyStoriesV2-GPT4-valid.txt")).unwrap();
807
+ let input_bytes = input.as_bytes();
808
+
809
+ let next_count = SimdPretokIter::new(input_bytes).fold(0usize, |c, _| c + 1);
810
+ let count_count = SimdPretokIter::new(input_bytes).count();
811
+ assert_eq!(
812
+ next_count, count_count,
813
+ "count() = {count_count} but next()-based count = {next_count}"
814
+ );
815
+ }
816
+
817
+ #[test]
818
+ fn simd_matches_fast() {
819
+ let data_dir = std::env::home_dir().unwrap().join("data");
820
+ let input =
821
+ std::fs::read_to_string(data_dir.join("TinyStoriesV2-GPT4-valid.txt")).unwrap();
822
+ let input_bytes = input.as_bytes();
823
+
824
+ let standard = crate::pretokenize::pretokenize_as_iter(input_bytes);
825
+ let simd = SimdPretokIter::new(input_bytes);
826
+
827
+ for eorb in standard.zip_longest(simd) {
828
+ match eorb {
829
+ itertools::EitherOrBoth::Both(a, b) => {
830
+ if a.0 != b.0 {
831
+ let a_offset =
832
+ a.0.as_ptr() as usize - input_bytes.as_ptr() as usize;
833
+ let region = &input_bytes[a_offset.saturating_sub(32)
834
+ ..min(input_bytes.len(), a_offset + 64)];
835
+ panic!(
836
+ "Mismatch at byte {a_offset}:\n standard: {:?}\n simd: {:?}\n context: {:?}",
837
+ String::from_utf8_lossy(a.0),
838
+ String::from_utf8_lossy(b.0),
839
+ String::from_utf8_lossy(region),
840
+ );
841
+ }
842
+ }
843
+ itertools::EitherOrBoth::Left(a) => {
844
+ panic!("Standard extra: {:?}", String::from_utf8_lossy(a.0));
845
+ }
846
+ itertools::EitherOrBoth::Right(b) => {
847
+ panic!("SIMD extra: {:?}", String::from_utf8_lossy(b.0));
848
+ }
849
+ }
850
+ }
851
+ }
852
+ }