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.
- checksums.yaml +7 -0
- data/Cargo.lock +3016 -0
- data/Cargo.toml +135 -0
- data/LICENSE +21 -0
- data/README.md +141 -0
- data/exe/gigatoken +9 -0
- data/ext/gigatoken/Cargo.toml +24 -0
- data/ext/gigatoken/extconf.rb +19 -0
- data/ext/gigatoken/src/error.rs +20 -0
- data/ext/gigatoken/src/gvl.rs +122 -0
- data/ext/gigatoken/src/lib.rs +50 -0
- data/ext/gigatoken/src/sentencepiece.rs +205 -0
- data/ext/gigatoken/src/sources.rs +207 -0
- data/ext/gigatoken/src/tokenizer.rs +571 -0
- data/lib/gigatoken/cli/bench.rb +64 -0
- data/lib/gigatoken/cli/support.rb +132 -0
- data/lib/gigatoken/cli/validate.rb +55 -0
- data/lib/gigatoken/cli.rb +18 -0
- data/lib/gigatoken/hub.rb +242 -0
- data/lib/gigatoken/packed_result.rb +48 -0
- data/lib/gigatoken/tokenizer.rb +117 -0
- data/lib/gigatoken/version.rb +5 -0
- data/lib/gigatoken.rb +29 -0
- data/rust-toolchain.toml +8 -0
- data/src/batch.rs +1808 -0
- data/src/bindings/bridge.rs +396 -0
- data/src/bindings/hub.rs +42 -0
- data/src/bindings/matcher.rs +114 -0
- data/src/bindings/mod.rs +14 -0
- data/src/bindings/padding.rs +177 -0
- data/src/bindings/pretokenize.rs +53 -0
- data/src/bindings/sources.rs +273 -0
- data/src/bindings/train.rs +125 -0
- data/src/bpe/mod.rs +1217 -0
- data/src/bpe/pretoken_cache.rs +495 -0
- data/src/bpe/sentencepiece.rs +1485 -0
- data/src/bpe/tiktoken.rs +2555 -0
- data/src/bpe_train.rs +351 -0
- data/src/input/decompress.rs +11 -0
- data/src/input/file_source.rs +514 -0
- data/src/input/jsonl.rs +94 -0
- data/src/input/mod.rs +333 -0
- data/src/input/parquet.rs +303 -0
- data/src/lib.rs +578 -0
- data/src/load_tokenizer/hf.rs +1036 -0
- data/src/load_tokenizer/hub.rs +344 -0
- data/src/load_tokenizer/mod.rs +3 -0
- data/src/load_tokenizer/tiktoken.rs +87 -0
- data/src/main.rs +95 -0
- data/src/pretokenize/fast/cl100k.rs +426 -0
- data/src/pretokenize/fast/cl100k_family.rs +891 -0
- data/src/pretokenize/fast/deepseek_v3.rs +605 -0
- data/src/pretokenize/fast/kimi.rs +281 -0
- data/src/pretokenize/fast/mask.rs +1486 -0
- data/src/pretokenize/fast/mod.rs +446 -0
- data/src/pretokenize/fast/nemotron.rs +138 -0
- data/src/pretokenize/fast/o200k.rs +347 -0
- data/src/pretokenize/fast/o200k_family.rs +1734 -0
- data/src/pretokenize/fast/olmo3.rs +505 -0
- data/src/pretokenize/fast/qwen2.rs +429 -0
- data/src/pretokenize/fast/qwen3_5.rs +541 -0
- data/src/pretokenize/fast/r50k.rs +1250 -0
- data/src/pretokenize/mod.rs +1079 -0
- data/src/pretokenize/options.rs +188 -0
- data/src/pretokenize/pretoken.rs +20 -0
- data/src/pretokenize/pretokenize_traits.rs +49 -0
- data/src/pretokenize/reference/avx512.rs +522 -0
- data/src/pretokenize/reference/combinator.rs +572 -0
- data/src/pretokenize/reference/mod.rs +28 -0
- data/src/pretokenize/reference/simd.rs +852 -0
- data/src/pretokenize/reference/state_machine.rs +365 -0
- data/src/pretokenize/unicode.rs +546 -0
- data/src/test_hub.rs +28 -0
- data/src/token.rs +42 -0
- metadata +161 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
//! Implement the regex
|
|
2
|
+
//! '(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"
|
|
3
|
+
//! using winnow parser combinators.
|
|
4
|
+
use crate::pretokenize::{Pretoken, unicode};
|
|
5
|
+
use std::cmp::min;
|
|
6
|
+
|
|
7
|
+
use eyre::Context;
|
|
8
|
+
use itertools::Itertools;
|
|
9
|
+
use rayon::prelude::*;
|
|
10
|
+
use winnow::Parser;
|
|
11
|
+
use winnow::combinator::{alt, iterator, trace};
|
|
12
|
+
use winnow::prelude::*;
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// NEON scan utilities — find the end of a run of matching ASCII bytes,
|
|
16
|
+
// processing 16 bytes at a time with precise exit via trailing_zeros.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
#[cfg(target_arch = "aarch64")]
|
|
20
|
+
mod neon {
|
|
21
|
+
use std::arch::aarch64::*;
|
|
22
|
+
|
|
23
|
+
/// Return the byte index (relative to the chunk start) of the first lane
|
|
24
|
+
/// where `mask` is non-zero, or 16 if all lanes are zero.
|
|
25
|
+
#[inline(always)]
|
|
26
|
+
unsafe fn first_nonzero_lane(mask: uint8x16_t) -> usize {
|
|
27
|
+
unsafe {
|
|
28
|
+
let lo = vgetq_lane_u64::<0>(vreinterpretq_u64_u8(mask));
|
|
29
|
+
if lo != 0 {
|
|
30
|
+
return (lo.trailing_zeros() as usize) / 8;
|
|
31
|
+
}
|
|
32
|
+
let hi = vgetq_lane_u64::<1>(vreinterpretq_u64_u8(mask));
|
|
33
|
+
if hi != 0 {
|
|
34
|
+
return 8 + (hi.trailing_zeros() as usize) / 8;
|
|
35
|
+
}
|
|
36
|
+
16
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Scan ASCII letters: `(b | 0x20) - 'a' < 26`.
|
|
41
|
+
#[inline]
|
|
42
|
+
pub unsafe fn scan_ascii_letters(bytes: &[u8], start: usize) -> usize {
|
|
43
|
+
unsafe { scan_generic(bytes, start, is_ascii_letter_mask) }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Scan ASCII digits: `b - '0' < 10`.
|
|
47
|
+
#[inline]
|
|
48
|
+
pub unsafe fn scan_ascii_digits(bytes: &[u8], start: usize) -> usize {
|
|
49
|
+
unsafe { scan_generic(bytes, start, is_ascii_digit_mask) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Scan ASCII "other" bytes: not letter, not digit, not whitespace, not >= 0x80.
|
|
53
|
+
#[inline]
|
|
54
|
+
pub unsafe fn scan_ascii_other(bytes: &[u8], start: usize) -> usize {
|
|
55
|
+
unsafe { scan_generic(bytes, start, is_ascii_other_mask) }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Generic NEON scanner. `classify` returns a mask with 0xFF for matching bytes.
|
|
59
|
+
/// Returns the index of the first non-matching byte.
|
|
60
|
+
#[inline(always)]
|
|
61
|
+
unsafe fn scan_generic(
|
|
62
|
+
bytes: &[u8],
|
|
63
|
+
start: usize,
|
|
64
|
+
classify: unsafe fn(uint8x16_t) -> uint8x16_t,
|
|
65
|
+
) -> usize {
|
|
66
|
+
let mut i = start;
|
|
67
|
+
unsafe {
|
|
68
|
+
while i + 16 <= bytes.len() {
|
|
69
|
+
let chunk = vld1q_u8(bytes.as_ptr().add(i));
|
|
70
|
+
let is_match = classify(chunk);
|
|
71
|
+
let not_match = vmvnq_u8(is_match);
|
|
72
|
+
let pos = first_nonzero_lane(not_match);
|
|
73
|
+
if pos < 16 {
|
|
74
|
+
return i + pos;
|
|
75
|
+
}
|
|
76
|
+
i += 16;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
i // caller handles the scalar tail
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[inline(always)]
|
|
83
|
+
unsafe fn is_ascii_letter_mask(chunk: uint8x16_t) -> uint8x16_t {
|
|
84
|
+
unsafe {
|
|
85
|
+
let lower = vorrq_u8(chunk, vdupq_n_u8(0x20));
|
|
86
|
+
let sub = vsubq_u8(lower, vdupq_n_u8(b'a'));
|
|
87
|
+
vcgtq_u8(vdupq_n_u8(26), sub) // 0xFF if letter
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[inline(always)]
|
|
92
|
+
unsafe fn is_ascii_digit_mask(chunk: uint8x16_t) -> uint8x16_t {
|
|
93
|
+
unsafe {
|
|
94
|
+
let sub = vsubq_u8(chunk, vdupq_n_u8(b'0'));
|
|
95
|
+
vcgtq_u8(vdupq_n_u8(10), sub)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[inline(always)]
|
|
100
|
+
unsafe fn is_ascii_other_mask(chunk: uint8x16_t) -> uint8x16_t {
|
|
101
|
+
unsafe {
|
|
102
|
+
// "other" = NOT letter AND NOT digit AND NOT whitespace AND NOT non-ASCII
|
|
103
|
+
let is_letter = is_ascii_letter_mask(chunk);
|
|
104
|
+
let is_digit = is_ascii_digit_mask(chunk);
|
|
105
|
+
// Whitespace: 9-13 or 32
|
|
106
|
+
let ws_sub = vsubq_u8(chunk, vdupq_n_u8(9));
|
|
107
|
+
let is_ws_ctrl = vcgeq_u8(vdupq_n_u8(4), ws_sub); // 9..=13
|
|
108
|
+
let is_space = vceqq_u8(chunk, vdupq_n_u8(b' '));
|
|
109
|
+
let is_ws = vorrq_u8(is_ws_ctrl, is_space);
|
|
110
|
+
// Non-ASCII (>= 0x80)
|
|
111
|
+
let is_high = vcgeq_u8(chunk, vdupq_n_u8(0x80));
|
|
112
|
+
// Other = none of the above
|
|
113
|
+
let any_exclude = vorrq_u8(vorrq_u8(is_letter, is_digit), vorrq_u8(is_ws, is_high));
|
|
114
|
+
vmvnq_u8(any_exclude) // 0xFF where "other"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Portable fallbacks
|
|
120
|
+
#[cfg(not(target_arch = "aarch64"))]
|
|
121
|
+
mod neon {
|
|
122
|
+
#[inline]
|
|
123
|
+
pub unsafe fn scan_ascii_letters(bytes: &[u8], start: usize) -> usize {
|
|
124
|
+
start // fall through to scalar immediately
|
|
125
|
+
}
|
|
126
|
+
#[inline]
|
|
127
|
+
pub unsafe fn scan_ascii_digits(bytes: &[u8], start: usize) -> usize {
|
|
128
|
+
start
|
|
129
|
+
}
|
|
130
|
+
#[inline]
|
|
131
|
+
pub unsafe fn scan_ascii_other(bytes: &[u8], start: usize) -> usize {
|
|
132
|
+
start
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Scalar helpers
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
#[inline(always)]
|
|
141
|
+
fn backtrack() -> winnow::error::ErrMode<winnow::error::ContextError> {
|
|
142
|
+
winnow::error::ErrMode::Backtrack(winnow::error::ContextError::new())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Decode one char from a non-ASCII position in a byte slice.
|
|
146
|
+
/// Caller guarantees `bytes[0] >= 0x80` and `bytes` is valid UTF-8.
|
|
147
|
+
#[inline(always)]
|
|
148
|
+
unsafe fn decode_non_ascii(bytes: &[u8]) -> char {
|
|
149
|
+
unsafe {
|
|
150
|
+
std::str::from_utf8_unchecked(bytes)
|
|
151
|
+
.chars()
|
|
152
|
+
.next()
|
|
153
|
+
.unwrap_unchecked()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Individual parsers — each returns () and advances *input on success.
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
fn contraction(input: &mut &str) -> ModalResult<()> {
|
|
162
|
+
let bytes = input.as_bytes();
|
|
163
|
+
if bytes.first() != Some(&b'\'') {
|
|
164
|
+
return Err(backtrack());
|
|
165
|
+
}
|
|
166
|
+
let advance = match bytes.get(1) {
|
|
167
|
+
Some(b's' | b'd' | b'm' | b't') => 2,
|
|
168
|
+
Some(b'l') if bytes.get(2) == Some(&b'l') => 3,
|
|
169
|
+
Some(b'v') if bytes.get(2) == Some(&b'e') => 3,
|
|
170
|
+
Some(b'r') if bytes.get(2) == Some(&b'e') => 3,
|
|
171
|
+
_ => return Err(backtrack()),
|
|
172
|
+
};
|
|
173
|
+
*input = &input[advance..];
|
|
174
|
+
Ok(())
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn letter_run(input: &mut &str) -> ModalResult<()> {
|
|
178
|
+
let before = *input;
|
|
179
|
+
let bytes = before.as_bytes();
|
|
180
|
+
let mut i = if bytes.first() == Some(&b' ') { 1 } else { 0 };
|
|
181
|
+
let letter_start = i;
|
|
182
|
+
|
|
183
|
+
// NEON fast path (returns precise position of first non-ASCII-letter)
|
|
184
|
+
i = unsafe { neon::scan_ascii_letters(bytes, i) };
|
|
185
|
+
// Scalar tail for remaining < 16 bytes
|
|
186
|
+
while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
|
|
187
|
+
i += 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Handle non-ASCII unicode letters, resuming fast scan after each one
|
|
191
|
+
while i < bytes.len() && bytes[i] >= 0x80 {
|
|
192
|
+
let c = unsafe { decode_non_ascii(&bytes[i..]) };
|
|
193
|
+
if unicode::is_letter(c) {
|
|
194
|
+
i += c.len_utf8();
|
|
195
|
+
i = unsafe { neon::scan_ascii_letters(bytes, i) };
|
|
196
|
+
while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
|
|
197
|
+
i += 1;
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if i == letter_start {
|
|
205
|
+
return Err(backtrack());
|
|
206
|
+
}
|
|
207
|
+
*input = &before[i..];
|
|
208
|
+
Ok(())
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fn number_run(input: &mut &str) -> ModalResult<()> {
|
|
212
|
+
let before = *input;
|
|
213
|
+
let bytes = before.as_bytes();
|
|
214
|
+
let mut i = if bytes.first() == Some(&b' ') { 1 } else { 0 };
|
|
215
|
+
let start = i;
|
|
216
|
+
|
|
217
|
+
i = unsafe { neon::scan_ascii_digits(bytes, i) };
|
|
218
|
+
while i < bytes.len() && bytes[i].is_ascii_digit() {
|
|
219
|
+
i += 1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
while i < bytes.len() && bytes[i] >= 0x80 {
|
|
223
|
+
let c = unsafe { decode_non_ascii(&bytes[i..]) };
|
|
224
|
+
if unicode::is_number(c) {
|
|
225
|
+
i += c.len_utf8();
|
|
226
|
+
i = unsafe { neon::scan_ascii_digits(bytes, i) };
|
|
227
|
+
while i < bytes.len() && bytes[i].is_ascii_digit() {
|
|
228
|
+
i += 1;
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if i == start {
|
|
236
|
+
return Err(backtrack());
|
|
237
|
+
}
|
|
238
|
+
*input = &before[i..];
|
|
239
|
+
Ok(())
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fn other_run(input: &mut &str) -> ModalResult<()> {
|
|
243
|
+
let before = *input;
|
|
244
|
+
let bytes = before.as_bytes();
|
|
245
|
+
let mut i = if bytes.first() == Some(&b' ') { 1 } else { 0 };
|
|
246
|
+
let start = i;
|
|
247
|
+
|
|
248
|
+
i = unsafe { neon::scan_ascii_other(bytes, i) };
|
|
249
|
+
while i < bytes.len() {
|
|
250
|
+
let b = bytes[i];
|
|
251
|
+
if b < 0x80 {
|
|
252
|
+
if !b.is_ascii_alphanumeric() && !b.is_ascii_whitespace() {
|
|
253
|
+
i += 1;
|
|
254
|
+
} else {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Handle non-ASCII "other" chars (unicode symbols, etc.)
|
|
263
|
+
while i < bytes.len() && bytes[i] >= 0x80 {
|
|
264
|
+
let c = unsafe { decode_non_ascii(&bytes[i..]) };
|
|
265
|
+
if unicode::is_other_complete(c) {
|
|
266
|
+
i += c.len_utf8();
|
|
267
|
+
i = unsafe { neon::scan_ascii_other(bytes, i) };
|
|
268
|
+
while i < bytes.len() {
|
|
269
|
+
let b = bytes[i];
|
|
270
|
+
if b < 0x80 && !b.is_ascii_alphanumeric() && !b.is_ascii_whitespace() {
|
|
271
|
+
i += 1;
|
|
272
|
+
} else {
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if i == start {
|
|
282
|
+
return Err(backtrack());
|
|
283
|
+
}
|
|
284
|
+
*input = &before[i..];
|
|
285
|
+
Ok(())
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
fn whitespace_run(input: &mut &str) -> ModalResult<()> {
|
|
289
|
+
let before = *input;
|
|
290
|
+
let bytes = before.as_bytes();
|
|
291
|
+
let mut i = 0;
|
|
292
|
+
while i < bytes.len() {
|
|
293
|
+
let b = bytes[i];
|
|
294
|
+
if b.is_ascii_whitespace() {
|
|
295
|
+
i += 1;
|
|
296
|
+
} else if b >= 0x80 {
|
|
297
|
+
let c = unsafe { decode_non_ascii(&bytes[i..]) };
|
|
298
|
+
if unicode::is_whitespace(c) {
|
|
299
|
+
i += c.len_utf8();
|
|
300
|
+
} else {
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if i == 0 {
|
|
308
|
+
return Err(backtrack());
|
|
309
|
+
}
|
|
310
|
+
// At end of input the `(?!\S)` lookahead succeeds, so `\s+(?!\S)` consumes
|
|
311
|
+
// the entire trailing whitespace run as one pretoken.
|
|
312
|
+
if i >= bytes.len() {
|
|
313
|
+
*input = &before[i..];
|
|
314
|
+
return Ok(());
|
|
315
|
+
}
|
|
316
|
+
// Find start of last whitespace char
|
|
317
|
+
let mut last_start = i - 1;
|
|
318
|
+
while last_start > 0 && bytes[last_start] & 0xC0 == 0x80 {
|
|
319
|
+
last_start -= 1;
|
|
320
|
+
}
|
|
321
|
+
// Need at least 2 ws chars (last_start > 0 means there's content before the last char)
|
|
322
|
+
if last_start == 0 {
|
|
323
|
+
return Err(backtrack());
|
|
324
|
+
}
|
|
325
|
+
// Leave last ws char for single_whitespace
|
|
326
|
+
*input = &before[last_start..];
|
|
327
|
+
Ok(())
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
fn single_whitespace(input: &mut &str) -> ModalResult<()> {
|
|
331
|
+
let bytes = input.as_bytes();
|
|
332
|
+
let b = *bytes.first().ok_or_else(backtrack)?;
|
|
333
|
+
if b.is_ascii_whitespace() {
|
|
334
|
+
*input = &input[1..];
|
|
335
|
+
Ok(())
|
|
336
|
+
} else if b >= 0x80 {
|
|
337
|
+
let c = unsafe { decode_non_ascii(bytes) };
|
|
338
|
+
if unicode::is_whitespace(c) {
|
|
339
|
+
*input = &input[c.len_utf8()..];
|
|
340
|
+
Ok(())
|
|
341
|
+
} else {
|
|
342
|
+
Err(backtrack())
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
Err(backtrack())
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// Top-level pretoken parser — direct dispatch on first byte instead of alt().
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
fn pretoken<'a>(input: &mut &'a str) -> ModalResult<Pretoken<'a>> {
|
|
354
|
+
let before = *input;
|
|
355
|
+
let bytes = before.as_bytes();
|
|
356
|
+
let first = *bytes.first().ok_or_else(backtrack)?;
|
|
357
|
+
|
|
358
|
+
if first < 0x80 {
|
|
359
|
+
if first.is_ascii_alphabetic() {
|
|
360
|
+
letter_run(input)?;
|
|
361
|
+
} else if first == b' ' {
|
|
362
|
+
// Peek at second byte to avoid cascading alt() failures
|
|
363
|
+
match bytes.get(1) {
|
|
364
|
+
Some(&b) if b.is_ascii_alphabetic() => letter_run(input)?,
|
|
365
|
+
Some(&b) if b.is_ascii_digit() => number_run(input)?,
|
|
366
|
+
Some(&b) if !b.is_ascii_whitespace() && b < 0x80 => {
|
|
367
|
+
other_run(input)?
|
|
368
|
+
}
|
|
369
|
+
Some(&b) if b >= 0x80 => {
|
|
370
|
+
let c = unsafe { decode_non_ascii(&bytes[1..]) };
|
|
371
|
+
if unicode::is_letter(c) {
|
|
372
|
+
letter_run(input)?;
|
|
373
|
+
} else if unicode::is_number(c) {
|
|
374
|
+
number_run(input)?;
|
|
375
|
+
} else if unicode::is_whitespace(c) {
|
|
376
|
+
ws_or_single(input)?;
|
|
377
|
+
} else {
|
|
378
|
+
other_run(input)?;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
_ => ws_or_single(input)?,
|
|
382
|
+
}
|
|
383
|
+
} else if first.is_ascii_digit() {
|
|
384
|
+
number_run(input)?;
|
|
385
|
+
} else if first == b'\'' {
|
|
386
|
+
contraction(input).or_else(|_| other_run(input))?;
|
|
387
|
+
} else if first.is_ascii_whitespace() {
|
|
388
|
+
ws_or_single(input)?;
|
|
389
|
+
} else {
|
|
390
|
+
other_run(input)?;
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
let c = unsafe { decode_non_ascii(bytes) };
|
|
394
|
+
if unicode::is_letter(c) {
|
|
395
|
+
letter_run(input)?;
|
|
396
|
+
} else if unicode::is_number(c) {
|
|
397
|
+
number_run(input)?;
|
|
398
|
+
} else if unicode::is_whitespace(c) {
|
|
399
|
+
ws_or_single(input)?;
|
|
400
|
+
} else {
|
|
401
|
+
other_run(input)?;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let consumed = before.len() - input.len();
|
|
406
|
+
Ok(Pretoken(&before.as_bytes()[..consumed]))
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// Try whitespace_run first (2+ ws chars with non-ws following), fall back to single.
|
|
410
|
+
#[inline]
|
|
411
|
+
fn ws_or_single(input: &mut &str) -> ModalResult<()> {
|
|
412
|
+
whitespace_run(input).or_else(|_| single_whitespace(input))
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// Public API
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
pub struct PretokenIterator<'a> {
|
|
420
|
+
input: &'a [u8],
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/// Parse the next pretoken from `input`, advancing the slice past the consumed bytes.
|
|
424
|
+
/// Returns `None` when `input` is empty.
|
|
425
|
+
pub fn pretoken_next<'a>(input: &mut &'a str) -> Option<Pretoken<'a>> {
|
|
426
|
+
if input.is_empty() {
|
|
427
|
+
return None;
|
|
428
|
+
}
|
|
429
|
+
pretoken(input).ok()
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// The `impl FnMut` parser type is unnameable, so this signature can't be
|
|
433
|
+
// factored into a type alias.
|
|
434
|
+
#[allow(clippy::type_complexity)]
|
|
435
|
+
pub fn pretokens_iterator<'a>(
|
|
436
|
+
input: &'a mut &'a str,
|
|
437
|
+
) -> winnow::combinator::ParserIterator<
|
|
438
|
+
'a,
|
|
439
|
+
impl FnMut(
|
|
440
|
+
&mut &'a str,
|
|
441
|
+
)
|
|
442
|
+
-> std::result::Result<Pretoken<'a>, winnow::error::ErrMode<winnow::error::ContextError>>
|
|
443
|
+
+ 'a,
|
|
444
|
+
&'a str,
|
|
445
|
+
Pretoken<'a>,
|
|
446
|
+
winnow::error::ErrMode<winnow::error::ContextError>,
|
|
447
|
+
> {
|
|
448
|
+
iterator(input, pretoken)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
#[cfg(test)]
|
|
452
|
+
mod tests {
|
|
453
|
+
use super::*;
|
|
454
|
+
|
|
455
|
+
/// Trailing-whitespace and mixed-whitespace edge cases, checked against the
|
|
456
|
+
/// reference GPT-2 regex. Catches the `\s+(?!\S)` end-of-input case: the
|
|
457
|
+
/// lookahead succeeds at EOF, so a trailing run like "\n\n\n" must be a
|
|
458
|
+
/// single pretoken, not one per character.
|
|
459
|
+
#[test]
|
|
460
|
+
fn whitespace_edge_cases_match_regex() {
|
|
461
|
+
let re = fancy_regex::Regex::new(
|
|
462
|
+
r"'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+",
|
|
463
|
+
)
|
|
464
|
+
.unwrap();
|
|
465
|
+
let cases = [
|
|
466
|
+
"\n\n\n",
|
|
467
|
+
"\n\n\nhello",
|
|
468
|
+
"a\n\n\nb",
|
|
469
|
+
"hello \n world",
|
|
470
|
+
" \t\n ",
|
|
471
|
+
"x ",
|
|
472
|
+
"x \n",
|
|
473
|
+
" \n x",
|
|
474
|
+
"\t\t\tword",
|
|
475
|
+
"end\n\n",
|
|
476
|
+
"end\n",
|
|
477
|
+
" ",
|
|
478
|
+
"\u{a0}\u{a0}",
|
|
479
|
+
"word\u{2028}\u{2028}",
|
|
480
|
+
];
|
|
481
|
+
for case in cases {
|
|
482
|
+
let expected: Vec<&str> = re.find_iter(case).map(|m| m.unwrap().as_str()).collect();
|
|
483
|
+
|
|
484
|
+
let mut input = case;
|
|
485
|
+
let mut combinator: Vec<String> = Vec::new();
|
|
486
|
+
while let Some(p) = pretoken_next(&mut input) {
|
|
487
|
+
combinator.push(String::from_utf8(p.0.to_vec()).unwrap());
|
|
488
|
+
}
|
|
489
|
+
assert_eq!(combinator, expected, "combinator mismatch for {case:?}");
|
|
490
|
+
|
|
491
|
+
let mut fast: Vec<String> = Vec::new();
|
|
492
|
+
let mut it =
|
|
493
|
+
crate::pretokenize::fast::FastR50kPretokenizer::new(case.as_bytes());
|
|
494
|
+
for p in it {
|
|
495
|
+
fast.push(String::from_utf8(p.as_ref().to_vec()).unwrap());
|
|
496
|
+
}
|
|
497
|
+
assert_eq!(fast, expected, "fast mismatch for {case:?}");
|
|
498
|
+
|
|
499
|
+
let state_machine: Vec<String> =
|
|
500
|
+
crate::pretokenize::PretokenizerIter::new(case.as_bytes())
|
|
501
|
+
.map(|p| String::from_utf8(p.0.to_vec()).unwrap())
|
|
502
|
+
.collect();
|
|
503
|
+
assert_eq!(
|
|
504
|
+
state_machine, expected,
|
|
505
|
+
"state machine mismatch for {case:?}"
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
#[test]
|
|
511
|
+
fn combinator_compare() {
|
|
512
|
+
let data_dir = std::env::home_dir().unwrap().join("data");
|
|
513
|
+
let input = std::fs::read_to_string(data_dir.join("TinyStoriesV2-GPT4-valid.txt")).unwrap();
|
|
514
|
+
let input_bytes = input.as_bytes();
|
|
515
|
+
let standard_iterator = crate::pretokenize::pretokenize_as_iter(input_bytes);
|
|
516
|
+
let mut input_slice = input.as_str();
|
|
517
|
+
let mut combinator_iterator = pretokens_iterator(&mut input_slice);
|
|
518
|
+
for eorb in standard_iterator.zip_longest(&mut combinator_iterator) {
|
|
519
|
+
match eorb {
|
|
520
|
+
itertools::EitherOrBoth::Both(a, b) => {
|
|
521
|
+
if a.0 != b.0 {
|
|
522
|
+
eprintln!(
|
|
523
|
+
"Mismatch: {:?} != {:?}",
|
|
524
|
+
String::from_utf8_lossy(a.0),
|
|
525
|
+
String::from_utf8_lossy(b.0)
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
// Find text before and after the mismatch by comparing pointers from a.0 and input_bytes
|
|
529
|
+
let a_start = a.0.as_ptr() as usize;
|
|
530
|
+
let b_start = b.0.as_ptr() as usize;
|
|
531
|
+
let input_start = input_bytes.as_ptr() as usize;
|
|
532
|
+
let a_offset = a_start - input_start;
|
|
533
|
+
|
|
534
|
+
let region = &input_bytes
|
|
535
|
+
[a_offset.saturating_sub(32)..min(input_bytes.len(), a_offset + 32)];
|
|
536
|
+
eprintln!("Context: {:?}", String::from_utf8_lossy(region));
|
|
537
|
+
|
|
538
|
+
panic!("combinator pretoken differs from standard pretokenizer");
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
itertools::EitherOrBoth::Left(a) => {
|
|
542
|
+
eprintln!("Left only: {:?}", String::from_utf8_lossy(a.0));
|
|
543
|
+
|
|
544
|
+
// Find text before and after the mismatch by comparing pointers from a.0 and input_bytes
|
|
545
|
+
let a_start = a.0.as_ptr() as usize;
|
|
546
|
+
let input_start = input_bytes.as_ptr() as usize;
|
|
547
|
+
let a_offset = a_start - input_start;
|
|
548
|
+
|
|
549
|
+
let region = &input_bytes
|
|
550
|
+
[a_offset.saturating_sub(32)..min(input_bytes.len(), a_offset + 32)];
|
|
551
|
+
eprintln!("Context: {:?}", String::from_utf8_lossy(region));
|
|
552
|
+
|
|
553
|
+
panic!("standard pretokenizer produced an extra pretoken");
|
|
554
|
+
}
|
|
555
|
+
itertools::EitherOrBoth::Right(b) => {
|
|
556
|
+
eprintln!("Right only: {:?}", String::from_utf8_lossy(b.0));
|
|
557
|
+
|
|
558
|
+
// Find text before and after the mismatch by comparing pointers from b and input_bytes
|
|
559
|
+
let b_start = b.as_ptr() as usize;
|
|
560
|
+
let input_start = input_bytes.as_ptr() as usize;
|
|
561
|
+
let b_offset = b_start - input_start;
|
|
562
|
+
|
|
563
|
+
let region = &input_bytes
|
|
564
|
+
[b_offset.saturating_sub(32)..min(input_bytes.len(), b_offset + 32)];
|
|
565
|
+
eprintln!("Context: {:?}", String::from_utf8_lossy(region));
|
|
566
|
+
|
|
567
|
+
panic!("combinator pretokenizer produced an extra pretoken");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//! Reference pretokenizers: benchmark baselines and test oracles only.
|
|
2
|
+
//!
|
|
3
|
+
//! Nothing in here runs in the library's encode path — the production
|
|
4
|
+
//! pretokenizers are the mask scanners in [`super::fast`] (see
|
|
5
|
+
//! `pretokenize_as_iter` / `PretokenizerType`). These implementations are
|
|
6
|
+
//! kept so `benches/pretokenize.rs` can race the production scanners
|
|
7
|
+
//! against the designs they replaced, and as independent oracles in
|
|
8
|
+
//! differential tests:
|
|
9
|
+
//!
|
|
10
|
+
//! - [`state_machine`]: the byte-class DFA, the original correctness
|
|
11
|
+
//! reference (also exercised from other modules' tests).
|
|
12
|
+
//! - [`combinator`]: winnow parser-combinator implementation.
|
|
13
|
+
//! - [`simd`]: first-generation portable-SIMD prototype.
|
|
14
|
+
//! - [`avx512`]: hand-rolled AVX-512 prototype. Compile-time gated on
|
|
15
|
+
//! AVX-512BW/VL (build with `-C target-cpu=native` or equivalent to
|
|
16
|
+
//! include it) — as a baseline it is deliberately not runtime-dispatched,
|
|
17
|
+
//! unlike the production scanners, which detect their tier at runtime on
|
|
18
|
+
//! any build.
|
|
19
|
+
|
|
20
|
+
#[cfg(all(
|
|
21
|
+
target_arch = "x86_64",
|
|
22
|
+
target_feature = "avx512bw",
|
|
23
|
+
target_feature = "avx512vl"
|
|
24
|
+
))]
|
|
25
|
+
pub mod avx512;
|
|
26
|
+
pub mod combinator;
|
|
27
|
+
pub mod simd;
|
|
28
|
+
pub mod state_machine;
|