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,1079 @@
1
+ //! Pretokenization: split documents into pretokens following a tokenizer's
2
+ //! pretokenization regex.
3
+ //!
4
+ //! The production implementations live in `fast` (one submodule per scheme:
5
+ //! `fast::r50k` for GPT-2, `fast::cl100k` for GPT-4, ...), selected via
6
+ //! [`PretokenizerType`]. Superseded designs (state machine, combinator,
7
+ //! SIMD prototypes) live in [`reference`] as benchmark baselines and test
8
+ //! oracles — nothing there runs in the encode path.
9
+ //!
10
+ //! The main entry points are:
11
+ //! - `pretokenize_as_iter`: iterate pretokens of a `&[u8]` (r50k scheme)
12
+ //! - `PretokenizerType::pretokenize`: iterate pretokens of any scheme
13
+ //! - `Pretokenize` trait: `doc.pretokens()` on any `&[u8]`
14
+ //! - `pretokenize_par_bytes`: parallel pretokenization with document splitting and counting
15
+
16
+ pub(crate) use crate::pretokenize::pretoken::Pretoken;
17
+ use crate::pretokenize::pretokenize_traits::{
18
+ ParallelMergeCounts, PretokenCountable,
19
+ };
20
+ use crate::input::Resource;
21
+ use rayon::prelude::*;
22
+ use std::collections::HashMap;
23
+
24
+ pub mod fast;
25
+ mod options;
26
+ mod pretoken;
27
+ pub(crate) mod pretokenize_traits;
28
+ pub mod reference;
29
+ mod unicode;
30
+
31
+ pub use fast::{
32
+ FastCl100kPretokenizer, FastDeepSeekV3Pretokenizer, FastOlmo3Pretokenizer,
33
+ FastQwen2Pretokenizer, FastQwen35Pretokenizer, FastR50kPretokenizer,
34
+ };
35
+ pub use options::{FastPretokenizerDispatch, PretokenizerType};
36
+ pub use reference::state_machine::PretokenizerIter;
37
+
38
+ /// Default document separator used in common training corpora.
39
+ pub const DEFAULT_SEPARATOR: &[u8] = b"<|endoftext|>";
40
+
41
+ /// Iterate the pretokens of `bytes` using the production (r50k) pretokenizer.
42
+ #[inline]
43
+ pub fn pretokenize_as_iter(bytes: &[u8]) -> FastR50kPretokenizer<'_> {
44
+ FastR50kPretokenizer::new(bytes)
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Batched pretoken pulling (the encode loop's input interface)
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /// Chunk size of [`PretokenSpans::fill_spans_keyed`] — the live entries of
52
+ /// one [`SpanBatch`] fill.
53
+ pub const PRETOKEN_CHUNK: usize = 256;
54
+
55
+ /// Both 64-bit halves of the per-length pack mask, in scalar ALU ops. A
56
+ /// u128 `MAX >> s` lowers to a multi-instruction sequence and the 16-entry
57
+ /// table this replaces put a dependent L1 load (2.43% of process) on the
58
+ /// `n → key → store` chain; per-half variable shifts are single 1-cycle
59
+ /// ops, so the halves cost two independent 3-deep chains and no load port.
60
+ /// The length tag (`n << 120`) touches only the high half and is OR'd in
61
+ /// by the caller. `const`: the phase-B emission loop's `PACK_MASK_TABLE`
62
+ /// (see `fast::mask`) is built from this at compile time, so the ALU and
63
+ /// table forms cannot drift apart.
64
+ #[inline(always)]
65
+ pub(crate) const fn pack_mask_halves(n: usize) -> (u64, u64) {
66
+ debug_assert!(n >= 1 && n <= 15);
67
+ let s = (n * 8) as u32;
68
+ let lo = if n < 8 { u64::MAX >> (64u32.wrapping_sub(s) & 63) } else { u64::MAX };
69
+ let hi = if n > 8 { u64::MAX >> (128u32.wrapping_sub(s) & 63) } else { 0 };
70
+ (lo, hi)
71
+ }
72
+
73
+ // The key packers below (`pack_pretoken_key`, `fill_spans_keyed_with_buf`,
74
+ // phase B of the two-phase walker) read span bytes as native-endian words
75
+ // and mask, and the emit loop stores packed token lanes as one native-endian
76
+ // word — all little-endian layouts. A big-endian build would silently
77
+ // produce wrong keys and swapped tokens, so refuse to compile instead.
78
+ #[cfg(target_endian = "big")]
79
+ compile_error!("gigatoken's key packing and token-lane stores assume little-endian byte order");
80
+
81
+ /// Pack a pretoken of ≤ 15 bytes into a `u128` cache key: bytes in the low
82
+ /// 15 lanes, length in the top byte (so keys of different lengths never
83
+ /// collide, and a real key is never 0). Returns `None` for longer
84
+ /// pretokens, which use the slice-keyed fallback map.
85
+ ///
86
+ /// The common path is a single unaligned 16-byte load followed by a mask,
87
+ /// avoiding both a variable-length `memcpy` and per-byte branching. The
88
+ /// load is only taken when it cannot cross a page boundary, so it can
89
+ /// never touch an unmapped page; the rare near-boundary case falls back to
90
+ /// a plain copy. Both paths produce the identical key.
91
+ #[inline(always)]
92
+ pub(crate) fn pack_pretoken_key(bytes: &[u8]) -> Option<u128> {
93
+ let n = bytes.len();
94
+ if n > 15 {
95
+ return None;
96
+ }
97
+ if n == 0 {
98
+ // Empty pretokens (possible through the public API, never from a
99
+ // pretokenizer) pack to key 0, which the short table reserves as
100
+ // its empty sentinel — the encode loop routes key 0 to the long
101
+ // map. Also keeps the read below from touching a zero-length
102
+ // slice's dangling pointer.
103
+ return Some(0);
104
+ }
105
+ let p = bytes.as_ptr();
106
+ let low = if (p as usize) & 4095 <= 4096 - 16 {
107
+ // SAFETY: the offset within the (≥ 4096-byte) page is ≤ 4096 - 16,
108
+ // so a 16-byte read stays inside the page holding `p`, which is
109
+ // mapped because `p` points to at least one valid byte.
110
+ let v = unsafe { (p as *const u128).read_unaligned() };
111
+ let (mask_lo, mask_hi) = pack_mask_halves(n);
112
+ ((v as u64 & mask_lo) as u128) | ((((v >> 64) as u64 & mask_hi) as u128) << 64)
113
+ } else {
114
+ // Rare: `p` is within 16 bytes of a page boundary. Gather with a
115
+ // plain copy (≤ 15 bytes) — correctness over speed on this cold
116
+ // path. Lanes past `n` stay zero, so no mask is needed.
117
+ let mut lanes = [0u8; 16];
118
+ lanes[..n].copy_from_slice(bytes);
119
+ u128::from_le_bytes(lanes)
120
+ };
121
+ Some(low | ((n as u128) << 120))
122
+ }
123
+
124
+ /// Hash of a packed pretoken key. Quality is noncritical for correctness
125
+ /// (the table compares full keys), but every consumer — the fill loops
126
+ /// (`fill_spans_keyed_with{,_buf}`, `fill_spans_two_phase`),
127
+ /// `ShortPretokenCache::grow`'s rehash, and the vocab-seeding paths
128
+ /// (`seeded_pretoken_cache`, `add_special_token`, `fork_sized`) — must
129
+ /// compute the same function of the key: the arms below produce different
130
+ /// values and may never mix in one process image. On aarch64 the arm is
131
+ /// picked at compile time; on x86_64 it is picked per process by
132
+ /// [`crc_hash_selected`], an immutable pure function of the CPU — this
133
+ /// entry point branches on that bit (cheap at the cold/slow sites that
134
+ /// use it, including the test-only [`fill_spans_keyed_with`]), and the
135
+ /// two hot fill loops instead embed one arm per monomorphization,
136
+ /// dispatched once per fill on the same bit (see [`fill_span_hash`]), so
137
+ /// the same key always hashes the same way. All
138
+ /// arms map key 0 to hash 0, which the fill loops' long-pretoken route
139
+ /// stores.
140
+ #[inline(always)]
141
+ pub(crate) fn pretoken_key_hash(key: u128) -> u64 {
142
+ // Note: `crc` is in the default feature set for aarch64-apple-darwin
143
+ // but NOT for aarch64-unknown-linux-gnu — generic aarch64 Linux builds
144
+ // need `-C target-feature=+crc` (e.g. via RUSTFLAGS) to get this fast
145
+ // hash; without it they silently take the multiply fold below.
146
+ #[cfg(all(target_arch = "aarch64", target_feature = "crc"))]
147
+ {
148
+ // Hardware CRC32: two 3-cycle ops replace the 5-op multiply fold.
149
+ // Linear over GF(2), so the low bits (the table index) see every
150
+ // key bit; 32 bits suffice for any table under 2^32 slots.
151
+ use core::arch::aarch64::__crc32d;
152
+ // SAFETY: gated on the `crc` target feature at compile time.
153
+ unsafe { __crc32d(__crc32d(0, key as u64), (key >> 64) as u64) as u64 }
154
+ }
155
+ #[cfg(target_arch = "x86_64")]
156
+ {
157
+ if crc_hash_selected() {
158
+ // SAFETY: `crc_hash_selected` verified SSE4.2 support (or the
159
+ // build enables it statically, folding this branch away).
160
+ unsafe { pretoken_key_hash_crc32c(key) }
161
+ } else {
162
+ pretoken_key_hash_fold(key)
163
+ }
164
+ }
165
+ #[cfg(not(any(all(target_arch = "aarch64", target_feature = "crc"), target_arch = "x86_64")))]
166
+ {
167
+ pretoken_key_hash_fold(key)
168
+ }
169
+ }
170
+
171
+ /// The multiply-fold arm of [`pretoken_key_hash`]: one folded multiply,
172
+ /// the cheapest mix whose low bits still see every key bit. Every target
173
+ /// can execute it; it is the process's hash wherever no hardware CRC arm
174
+ /// applies. Maps key 0 to hash 0 (0 · M = 0).
175
+ #[allow(dead_code)] // no cfg arm references it under aarch64 + crc
176
+ #[inline(always)]
177
+ fn pretoken_key_hash_fold(key: u128) -> u64 {
178
+ let lo = key as u64;
179
+ let hi = (key >> 64) as u64;
180
+ let mut h = (lo ^ hi.rotate_right(25)).wrapping_mul(0x9E37_79B9_7F4A_7C15);
181
+ h ^= h >> 32;
182
+ h
183
+ }
184
+
185
+ /// The hardware CRC32C (SSE4.2) arm of [`pretoken_key_hash`]: same shape
186
+ /// and rationale as the aarch64 CRC32 arm — linear over GF(2) so the low
187
+ /// bits (the table index) see every key bit, 3-cycle latency and one µop
188
+ /// per `crc32` on Zen 2 (two chained ops vs the 5-op multiply fold), and
189
+ /// `_mm_crc32_u64(0, 0) == 0` preserves the key 0 -> hash 0 property the
190
+ /// fill loops' long-pretoken route stores.
191
+ ///
192
+ /// `sse4.2` is NOT in baseline x86-64, so distributed wheels cannot gate
193
+ /// this arm at compile time; it is instead selected per process by
194
+ /// [`crc_hash_selected`] and reached only through call sites guarded by
195
+ /// that bit. Builds with `sse4.2` statically enabled (`-C
196
+ /// target-cpu=znver2`, any x86-64-v2+ setting) fold the guards away and
197
+ /// keep the pure-CRC codegen of a compile-time arm.
198
+ ///
199
+ /// # Safety
200
+ ///
201
+ /// The CPU must support SSE4.2: callers reach this only after
202
+ /// [`crc_hash_selected`] returned true (directly, or structurally via a
203
+ /// `fill_span_hash::<true>` monomorphization — see its contract), or from
204
+ /// a build with `sse4.2` statically enabled.
205
+ #[cfg(target_arch = "x86_64")]
206
+ #[target_feature(enable = "sse4.2")]
207
+ #[inline]
208
+ unsafe fn pretoken_key_hash_crc32c(key: u128) -> u64 {
209
+ use core::arch::x86_64::_mm_crc32_u64;
210
+ // SAFETY: SSE4.2 is enabled on this function; the caller (per the
211
+ // contract above) only reaches it on a CPU that has it.
212
+ unsafe { _mm_crc32_u64(_mm_crc32_u64(0, key as u64), (key >> 64) as u64) }
213
+ }
214
+
215
+ /// Does this process hash pretoken keys with CRC32C (x86_64)? A pure
216
+ /// function of the CPU, so one immutable answer for the process lifetime:
217
+ /// [`pretoken_key_hash`] and every fill-loop dispatch branch on this same
218
+ /// bit, which is what keeps the one-hash-per-process invariant airtight
219
+ /// without a per-key runtime check in the hot loops. std caches the
220
+ /// CPUID result, so after the first call this is a relaxed atomic load +
221
+ /// bit test; builds with `sse4.2` statically enabled const-fold it to
222
+ /// `true`.
223
+ #[cfg(target_arch = "x86_64")]
224
+ #[inline(always)]
225
+ pub(crate) fn crc_hash_selected() -> bool {
226
+ std::arch::is_x86_feature_detected!("sse4.2")
227
+ }
228
+
229
+ /// Per-span hash for the monomorphized fill-loop bodies:
230
+ /// [`pretoken_key_hash`] with the x86_64 per-process selection hoisted
231
+ /// out of the per-span path. On x86_64 the two instantiations embed one
232
+ /// arm each, and each is reachable only under the matching value of
233
+ /// [`crc_hash_selected`]:
234
+ ///
235
+ /// - `X86_CRC = true` bodies are called exclusively from the
236
+ /// `#[target_feature(enable = "sse4.2")]` fill wrappers, which their
237
+ /// dispatchers enter only when `crc_hash_selected()` is true;
238
+ /// - `X86_CRC = false` bodies are called exclusively from the dispatchers'
239
+ /// other arm, i.e. only when `crc_hash_selected()` is false (with
240
+ /// `sse4.2` statically enabled that arm is statically dead).
241
+ ///
242
+ /// So every instantiation agrees with what [`pretoken_key_hash`] returns
243
+ /// for the same key in the same process. Off x86_64 the parameter is
244
+ /// ignored and this IS [`pretoken_key_hash`].
245
+ #[inline(always)]
246
+ pub(crate) fn fill_span_hash<const X86_CRC: bool>(key: u128) -> u64 {
247
+ #[cfg(target_arch = "x86_64")]
248
+ {
249
+ if X86_CRC {
250
+ // Reachability contract: only the sse4.2-gated fill wrappers
251
+ // instantiate `X86_CRC = true`, so the selection bit must hold.
252
+ debug_assert!(crc_hash_selected());
253
+ // SAFETY: the `true` instantiation is only reachable from the
254
+ // sse4.2-gated fill wrappers (see the contract above), so the
255
+ // CPU has SSE4.2.
256
+ unsafe { pretoken_key_hash_crc32c(key) }
257
+ } else {
258
+ pretoken_key_hash_fold(key)
259
+ }
260
+ }
261
+ #[cfg(not(target_arch = "x86_64"))]
262
+ {
263
+ pretoken_key_hash(key)
264
+ }
265
+ }
266
+
267
+ /// One batch slot: a pretoken span with its packed cache key and key hash,
268
+ /// as one 32-byte record (half a cache line, never straddling one thanks
269
+ /// to the alignment).
270
+ ///
271
+ /// AoS instead of the previous three parallel arrays for dataflow on both
272
+ /// sides: the fill loops store one record with two `stp`s on a single
273
+ /// store stream (the parallel arrays cost 4 store µops across 3 streams —
274
+ /// the split u128 key store alone was two), and the probe loop's per-`i`
275
+ /// `(key, hash)` read touches one cache line instead of three.
276
+ ///
277
+ /// `meta` carries the field the consumer needs next, keyed on `key`:
278
+ /// - `key != 0` (short pretoken, ≤ 15 bytes): `meta` is the full 64-bit
279
+ /// key hash. The span length rides in the key's top byte, so `ptr` +
280
+ /// `key >> 120` reconstructs the span on the (rare) slow path.
281
+ /// - `key == 0` (long pretoken, or an empty span through the public
282
+ /// adapter): `meta` is the span length in bytes. The hash is not stored:
283
+ /// the long route never probes the short table, and
284
+ /// `pretoken_key_hash(0) == 0` is what the old layout recorded anyway.
285
+ /// Prefetching `meta` as if it were a hash touches an arbitrary
286
+ /// (masked, in-bounds) table line — harmless, long pretokens are rare.
287
+ ///
288
+ /// Fields are `pub(crate)`: only the in-crate fill loops may write entries
289
+ /// (safe external writes of an arbitrary `ptr`/`key` would let safe code
290
+ /// drive [`SpanBatch::span`]'s `from_raw_parts` with garbage — see the
291
+ /// [`PretokenSpans`] safety contract).
292
+ #[derive(Clone, Copy)]
293
+ #[repr(C, align(32))]
294
+ pub(crate) struct BatchEntry {
295
+ pub(crate) key: u128,
296
+ pub(crate) ptr: *const u8,
297
+ pub(crate) meta: u64,
298
+ }
299
+
300
+ const _: () = assert!(std::mem::size_of::<BatchEntry>() == 32);
301
+
302
+ impl BatchEntry {
303
+ /// Span length, independent of the short/long route.
304
+ #[inline(always)]
305
+ pub(crate) fn span_len(&self) -> usize {
306
+ if self.key != 0 { (self.key >> 120) as usize } else { self.meta as usize }
307
+ }
308
+ }
309
+
310
+ /// Readable slack entries past a full chunk, so the emit loop's
311
+ /// prefetch-ahead `entries[i + D].meta` load needs no index clamp (a
312
+ /// per-pretoken `add + cmp + csel` in the hottest loop). Slack entries
313
+ /// are never written by a fill; prefetching a stale or zero `meta`
314
+ /// requests an arbitrary masked (in-bounds) table line — harmless.
315
+ pub(crate) const SPAN_BATCH_SLACK: usize = 16;
316
+
317
+ /// One chunk of pretoken spans with their packed cache keys (0 = longer
318
+ /// than 15 bytes, routed to the slice-keyed fallback map) and key hashes,
319
+ /// filled by [`PretokenSpans::fill_spans_keyed`]. See [`BatchEntry`] for
320
+ /// the record layout. Fills only ever write the first [`PRETOKEN_CHUNK`]
321
+ /// entries; the tail is prefetch slack (see [`SPAN_BATCH_SLACK`]).
322
+ pub struct SpanBatch<'a> {
323
+ /// `pub(crate)`: writable only by the in-crate fill loops, which uphold
324
+ /// the [`PretokenSpans`] safety contract on every entry they write.
325
+ pub(crate) entries: [BatchEntry; PRETOKEN_CHUNK + SPAN_BATCH_SLACK],
326
+ /// The entries' `ptr`s borrow the spans' backing storage.
327
+ _spans: std::marker::PhantomData<&'a [u8]>,
328
+ }
329
+
330
+ impl<'a> SpanBatch<'a> {
331
+ pub fn new() -> Self {
332
+ SpanBatch {
333
+ entries: [BatchEntry { key: 0, ptr: std::ptr::null(), meta: 0 };
334
+ PRETOKEN_CHUNK + SPAN_BATCH_SLACK],
335
+ _spans: std::marker::PhantomData,
336
+ }
337
+ }
338
+
339
+ /// Reconstruct entry `i`'s span.
340
+ ///
341
+ /// # Safety
342
+ /// Entry `i` must have been written by the most recent fill (`i` below
343
+ /// its returned count), so `ptr` still points at a live span of `'a`.
344
+ #[inline(always)]
345
+ pub unsafe fn span(&self, i: usize) -> &'a [u8] {
346
+ let e = &self.entries[i];
347
+ // SAFETY: per the contract, `ptr` points at `span_len()` live bytes.
348
+ unsafe { std::slice::from_raw_parts(e.ptr, e.span_len()) }
349
+ }
350
+ }
351
+
352
+ impl Default for SpanBatch<'_> {
353
+ fn default() -> Self {
354
+ Self::new()
355
+ }
356
+ }
357
+
358
+ /// A source of pretoken spans, pulled a chunk at a time with their cache
359
+ /// keys derived on the way out.
360
+ ///
361
+ /// `Tokenizer::memoized_encode` consumes pretokens through this instead of
362
+ /// `Iterator` for codegen reasons: pulling happens in a dedicated
363
+ /// out-of-line loop, so the pretokenizer state is register-allocated
364
+ /// across the whole chunk (inlined into the register-starved encode loop
365
+ /// it lives in stack slots, ~9 cycles/pretoken of spill traffic on Zen 2).
366
+ /// Key packing, hashing, and the cache-line prefetch ride along in the
367
+ /// same loop because the span walker is a serial dependency chain (IPC
368
+ /// ~1.7 standalone): the independent per-span key math fills its idle
369
+ /// issue slots nearly for free, where a separate pass paid for it in full.
370
+ ///
371
+ /// # Safety
372
+ ///
373
+ /// The consumer trusts every fill unconditionally: after
374
+ /// [`Self::fill_spans_keyed`] returns `n`, the emit loop calls
375
+ /// [`SpanBatch::span`] (a raw `from_raw_parts`) on any entry `i < n`.
376
+ /// Implementations must therefore uphold, for every call returning `n`:
377
+ ///
378
+ /// - `batch.entries[0..n]` were all written by THIS call (no stale entries
379
+ /// counted), and
380
+ /// - each written entry holds a valid `(ptr, len)` into caller-live input
381
+ /// bytes of lifetime `'a`: `ptr` non-null and readable for
382
+ /// [`BatchEntry::span_len`] bytes, with `key`/`meta` derived from exactly
383
+ /// those bytes via `pack_pretoken_key`/`pretoken_key_hash` semantics.
384
+ ///
385
+ /// The entry fields are `pub(crate)`, so implementations outside this
386
+ /// crate cannot write entries at all and can only soundly return 0; the
387
+ /// in-crate fill helpers (`fill_spans_keyed_with{,_buf}`,
388
+ /// `fill_spans_two_phase`) uphold the contract.
389
+ pub unsafe trait PretokenSpans<'a> {
390
+ /// Fill `batch` from the front with the next pretoken spans, calling
391
+ /// `prefetch(hash)` for each. Returns how many were written; a short
392
+ /// count (including 0) means the input is exhausted. (Recomputing the
393
+ /// hash at the consumer instead of storing it here measured 4% slower
394
+ /// end to end: the extra multiply sits on the probe loop's critical
395
+ /// path, while this loop has store slots to spare.)
396
+ fn fill_spans_keyed(&mut self, batch: &mut SpanBatch<'a>, prefetch: &impl Fn(u64)) -> usize;
397
+ }
398
+
399
+ /// Shared body of the iterator-backed [`PretokenSpans`] implementations
400
+ /// (spans with no single backing buffer): pull spans from `next` and derive
401
+ /// each one's key, hash, and prefetch on the way out. `#[inline(always)]`
402
+ /// so each `#[inline(never)]` implementation fuses it with its span walker
403
+ /// into a single out-of-line loop (see the trait docs for why that fusion
404
+ /// matters). Sources that walk one backing slice use
405
+ /// [`fill_spans_keyed_with_buf`] instead.
406
+ ///
407
+ /// No production caller walks this path — the concrete pretokenizers and
408
+ /// the dispatch enum all fill through [`fill_spans_keyed_with_buf`] or
409
+ /// `fast::fill_spans_two_phase` — so unlike those two, it takes no
410
+ /// CRC-monomorphized wrapper: [`pretoken_key_hash`]'s per-span dispatch
411
+ /// branch (same process-immutable [`crc_hash_selected`] bit, so hash
412
+ /// values agree with every other site) is irrelevant off the hot path.
413
+ #[inline(always)]
414
+ pub(crate) fn fill_spans_keyed_with<'a>(
415
+ mut next: impl FnMut() -> Option<&'a [u8]>,
416
+ batch: &mut SpanBatch<'a>,
417
+ prefetch: &impl Fn(u64),
418
+ ) -> usize {
419
+ let mut n = 0;
420
+ while n < PRETOKEN_CHUNK {
421
+ let Some(span) = next() else { break };
422
+ let (key, h) = match pack_pretoken_key(span) {
423
+ Some(key) => (key, pretoken_key_hash(key)),
424
+ None => (0, 0),
425
+ };
426
+ prefetch(h);
427
+ // Long (and empty) spans record their length; short spans their
428
+ // hash (see `BatchEntry::meta`).
429
+ let meta = if key != 0 { h } else { span.len() as u64 };
430
+ batch.entries[n] = BatchEntry { key, ptr: span.as_ptr(), meta };
431
+ n += 1;
432
+ }
433
+ n
434
+ }
435
+
436
+ /// [`fill_spans_keyed_with`] for span sources that walk a single backing
437
+ /// slice, with `next` yielding `(start, end)` byte offsets into `bytes`.
438
+ /// Knowing the buffer removes the two data-dependent branches of
439
+ /// [`pack_pretoken_key`] from the per-span path:
440
+ ///
441
+ /// - the `> 15` long-pretoken route becomes a select — for a long span the
442
+ /// 16-byte load sits entirely inside the span, so it needs no guard, and
443
+ /// the select (not the mask clamp) provides the key-0 routing;
444
+ /// - the per-span page-boundary check (mispredict-prone: ~0.4% of spans on
445
+ /// 4 KiB pages) becomes one buffer-end bound, hoisted per fill and false
446
+ /// only for short spans starting in the last 15 bytes of `bytes` — once
447
+ /// per input, so the branch predicts ~perfectly.
448
+ ///
449
+ /// Empty spans cannot occur (`next` contract: `start < end`), so the key-0
450
+ /// route is exactly "longer than 15 bytes", as in the fallible packer.
451
+ #[inline(always)]
452
+ pub(crate) fn fill_spans_keyed_with_buf<'a>(
453
+ bytes: &'a [u8],
454
+ next: impl FnMut() -> Option<(usize, usize)>,
455
+ batch: &mut SpanBatch<'a>,
456
+ prefetch: &impl Fn(u64),
457
+ ) -> usize {
458
+ // Hash-arm dispatch, once per fill — see [`fill_spans_keyed_with`].
459
+ #[cfg(target_arch = "x86_64")]
460
+ if crc_hash_selected() {
461
+ // SAFETY: `crc_hash_selected` verified SSE4.2 support.
462
+ return unsafe { fill_spans_keyed_with_buf_crc(bytes, next, batch, prefetch) };
463
+ }
464
+ fill_spans_keyed_with_buf_impl::<false>(bytes, next, batch, prefetch)
465
+ }
466
+
467
+ /// The SSE4.2 (CRC-hash) monomorphization of [`fill_spans_keyed_with_buf`].
468
+ ///
469
+ /// # Safety
470
+ ///
471
+ /// The CPU must support SSE4.2 ([`crc_hash_selected`] must have returned
472
+ /// true).
473
+ #[cfg(target_arch = "x86_64")]
474
+ #[target_feature(enable = "sse4.2")]
475
+ unsafe fn fill_spans_keyed_with_buf_crc<'a>(
476
+ bytes: &'a [u8],
477
+ next: impl FnMut() -> Option<(usize, usize)>,
478
+ batch: &mut SpanBatch<'a>,
479
+ prefetch: &impl Fn(u64),
480
+ ) -> usize {
481
+ fill_spans_keyed_with_buf_impl::<true>(bytes, next, batch, prefetch)
482
+ }
483
+
484
+ /// [`fill_spans_keyed_with_buf`]'s loop body, monomorphized on the hash
485
+ /// arm (`X86_CRC` — see [`fill_span_hash`]'s reachability contract).
486
+ #[inline(always)]
487
+ fn fill_spans_keyed_with_buf_impl<'a, const X86_CRC: bool>(
488
+ bytes: &'a [u8],
489
+ mut next: impl FnMut() -> Option<(usize, usize)>,
490
+ batch: &mut SpanBatch<'a>,
491
+ prefetch: &impl Fn(u64),
492
+ ) -> usize {
493
+ // A 16-byte load at `start` is in bounds iff `start < tail_lim`.
494
+ let tail_lim = bytes.len().saturating_sub(15);
495
+ let mut n = 0;
496
+ while n < PRETOKEN_CHUNK {
497
+ let Some((start, end)) = next() else { break };
498
+ debug_assert!(start < end && end <= bytes.len());
499
+ // SAFETY: `next` returns in-bounds span boundaries.
500
+ let span = unsafe { bytes.get_unchecked(start..end) };
501
+ let len = end - start;
502
+ let long = len > 15;
503
+ // `|` not `||`: one combined test, taken for all but the tail spans.
504
+ let key = if long | (start < tail_lim) {
505
+ // SAFETY: `start < tail_lim` puts `start + 16` within `bytes`;
506
+ // a long span (len ≥ 16) contains its own first 16 bytes.
507
+ let v = unsafe { (bytes.as_ptr().add(start) as *const u128).read_unaligned() };
508
+ let m = len.min(15);
509
+ let (mask_lo, mask_hi) = pack_mask_halves(m);
510
+ let lo = (v as u64) & mask_lo;
511
+ let hi = ((v >> 64) as u64 & mask_hi) | ((m as u64) << 56);
512
+ let packed = (lo as u128) | ((hi as u128) << 64);
513
+ if long { 0 } else { packed }
514
+ } else {
515
+ // Short span starting in the buffer's last 15 bytes: gather
516
+ // with a plain copy, once per input. Lanes past `len` stay
517
+ // zero, so no mask is needed.
518
+ let mut lanes = [0u8; 16];
519
+ lanes[..len].copy_from_slice(span);
520
+ u128::from_le_bytes(lanes) | ((len as u128) << 120)
521
+ };
522
+ let h = fill_span_hash::<X86_CRC>(key);
523
+ prefetch(h);
524
+ // Long spans record their length instead of the (unused) hash —
525
+ // see `BatchEntry::meta`.
526
+ let meta = if long { len as u64 } else { h };
527
+ batch.entries[n] = BatchEntry { key, ptr: span.as_ptr(), meta };
528
+ n += 1;
529
+ }
530
+ n
531
+ }
532
+
533
+ /// Adapter giving any pretoken iterator (reference pretokenizers, tests,
534
+ /// custom sources) the [`PretokenSpans`] interface. The `Fast*`
535
+ /// pretokenizers implement the trait directly over their walker state
536
+ /// instead (see `fast::fill_spans_keyed_mask`): routing them through
537
+ /// `Iterator::next` left the (large, `#[inline(always)]`) `next_span`
538
+ /// un-inlined behind a real call — measured cost in
539
+ /// `fast::fill_spans_keyed_mask`'s docs.
540
+ pub struct SpanIter<I>(pub I);
541
+
542
+ // SAFETY: delegates to `fill_spans_keyed_with`, which writes exactly the
543
+ // first `n` entries from the iterator's live `'a` spans.
544
+ unsafe impl<'a, I: Iterator<Item = Pretoken<'a>>> PretokenSpans<'a> for SpanIter<I> {
545
+ #[inline(never)]
546
+ fn fill_spans_keyed(&mut self, batch: &mut SpanBatch<'a>, prefetch: &impl Fn(u64)) -> usize {
547
+ fill_spans_keyed_with(|| self.0.next().map(|p| p.0), batch, prefetch)
548
+ }
549
+ }
550
+
551
+ // ---------------------------------------------------------------------------
552
+ // Pretokenize trait — Layer 3
553
+ // ---------------------------------------------------------------------------
554
+
555
+ /// Anything that can be split into a stream of pretokens.
556
+ pub trait Pretokenize {
557
+ fn pretokens(&self) -> FastR50kPretokenizer<'_>;
558
+ }
559
+
560
+ impl Pretokenize for [u8] {
561
+ fn pretokens(&self) -> FastR50kPretokenizer<'_> {
562
+ pretokenize_as_iter(self)
563
+ }
564
+ }
565
+
566
+ // ---------------------------------------------------------------------------
567
+ // Pretoken-safe document splitting
568
+ // ---------------------------------------------------------------------------
569
+
570
+ /// Split `bytes` into ranges of roughly `target` bytes whose boundaries are
571
+ /// pretoken boundaries under every supported pretokenization scheme, so
572
+ /// encoding the ranges independently and concatenating the token streams is
573
+ /// identical to encoding `bytes` in one pass.
574
+ ///
575
+ /// A boundary sits on a space that is preceded by an ASCII alphanumeric and
576
+ /// followed by an ASCII letter ("…word word…"). No scheme's pretoken can
577
+ /// cross such a point: whitespace only attaches to adjacent pretokens as a
578
+ /// single *leading* space of a following word (` ?\p{L}+` and friends), and
579
+ /// the only trailing attachments are `[\r\n]*`, which cannot contain a
580
+ /// space. Letter/digit runs cannot contain a space either, and the
581
+ /// all-whitespace rules (`\s+(?!\S)`, `\s*[\r\n]+`, …) never see a run that
582
+ /// crosses the boundary because the preceding byte is alphanumeric. The
583
+ /// three ASCII bytes also cannot sit inside a multi-byte UTF-8 character.
584
+ ///
585
+ /// `added_tokens` are the byte sequences matched atomically *before*
586
+ /// pretokenization (see `Tokenizer::encode_with_added_tokens`), each paired
587
+ /// with its `rstrip` flag; a candidate boundary is rejected when an
588
+ /// occurrence of one straddles it, since the halves would otherwise be
589
+ /// BPE-encoded as plain text. Only tokens that contain a space can ever
590
+ /// straddle a boundary (every boundary sits on a space byte), so for typical
591
+ /// vocabularies the check costs nothing. If no occurrence crosses a
592
+ /// boundary, greedy leftmost-longest matching restarted there reproduces the
593
+ /// single-pass matches: the matcher's only state is its scan position, and
594
+ /// no match can carry it across the boundary.
595
+ ///
596
+ /// An `rstrip` token absorbs the whitespace after its match, so a boundary
597
+ /// is also rejected when such an occurrence ends exactly at it — the space
598
+ /// opening the next chunk would encode as plain text instead of being
599
+ /// absorbed. (`lstrip` needs no counterpart: a boundary space preceding a
600
+ /// match lands at the start of the next chunk and is trimmed identically
601
+ /// there, and a boundary can never sit inside a longer whitespace run
602
+ /// because the byte before it must be alphanumeric.)
603
+ pub fn safe_split_ranges(
604
+ bytes: &[u8],
605
+ target: usize,
606
+ added_tokens: &[(&[u8], bool)],
607
+ ) -> Vec<std::ops::Range<usize>> {
608
+ let blockers: Vec<memchr::memmem::Finder> = added_tokens
609
+ .iter()
610
+ .filter(|(t, _)| t.contains(&b' '))
611
+ .map(|(t, _)| memchr::memmem::Finder::new(t))
612
+ .collect();
613
+ let max_blocker = blockers.iter().map(|f| f.needle().len()).max().unwrap_or(0);
614
+ // rstrip tokens can only end at a boundary when their last byte is the
615
+ // alphanumeric byte before the boundary space.
616
+ let end_blockers: Vec<&[u8]> = added_tokens
617
+ .iter()
618
+ .filter(|(t, rstrip)| *rstrip && t.last().is_some_and(u8::is_ascii_alphanumeric))
619
+ .map(|&(t, _)| t)
620
+ .collect();
621
+ // Whether an added-token occurrence spans the cut between `p - 1` and
622
+ // `p`. Such an occurrence must start within `max_blocker - 1` bytes
623
+ // before `p`, so searching a window of that radius is exhaustive.
624
+ let cuts_added_token = |p: usize| -> bool {
625
+ let lo = p.saturating_sub(max_blocker.saturating_sub(1));
626
+ let hi = (p + max_blocker.saturating_sub(1)).min(bytes.len());
627
+ blockers.iter().any(|f| {
628
+ f.find_iter(&bytes[lo..hi])
629
+ .any(|s| lo + s < p && lo + s + f.needle().len() > p)
630
+ })
631
+ };
632
+ // Whether an rstrip added-token occurrence ends exactly at `p`.
633
+ let ends_rstrip_token = |p: usize| end_blockers.iter().any(|t| bytes[..p].ends_with(t));
634
+ let len = bytes.len();
635
+ let target = target.max(1);
636
+ let mut out = Vec::new();
637
+ let mut start = 0;
638
+ 'chunks: while start < len {
639
+ let mut probe = start + target;
640
+ while probe + 1 < len {
641
+ if bytes[probe] == b' '
642
+ && bytes[probe - 1].is_ascii_alphanumeric()
643
+ && bytes[probe + 1].is_ascii_alphabetic()
644
+ && !(max_blocker > 0 && cuts_added_token(probe))
645
+ && !(!end_blockers.is_empty() && ends_rstrip_token(probe))
646
+ {
647
+ out.push(start..probe);
648
+ start = probe;
649
+ continue 'chunks;
650
+ }
651
+ probe += 1;
652
+ }
653
+ out.push(start..len);
654
+ break;
655
+ }
656
+ if out.is_empty() {
657
+ out.push(0..0);
658
+ }
659
+ out
660
+ }
661
+
662
+ // ---------------------------------------------------------------------------
663
+ // Parallel pretokenization with document splitting
664
+ // ---------------------------------------------------------------------------
665
+
666
+ /// Pretokenize `bytes` in parallel, splitting documents on `separator`.
667
+ /// Returns a map of pretoken → count.
668
+ pub fn pretokenize_par_bytes<'a>(
669
+ bytes: &'a [u8],
670
+ separator: &'a [u8],
671
+ ) -> HashMap<Pretoken<'a>, usize, rustc_hash::FxBuildHasher> {
672
+ let start_time = std::time::Instant::now();
673
+ let n_threads = rayon::current_num_threads();
674
+ eprintln!("Using {n_threads} threads for pretokenization");
675
+
676
+ let chunks = bytes.par_document_chunks(separator, n_threads);
677
+
678
+ let merged_counts = chunks
679
+ .into_par_iter()
680
+ .map(|doc_iter| {
681
+ doc_iter
682
+ .flat_map(|doc| doc.pretokens())
683
+ .pretoken_count()
684
+ })
685
+ .par_merge_counts();
686
+
687
+ let time_elapsed = start_time.elapsed();
688
+ eprintln!("Pretokenization took {time_elapsed:?}");
689
+
690
+ merged_counts
691
+ }
692
+
693
+
694
+ #[cfg(test)]
695
+ mod test {
696
+ use itertools::Itertools;
697
+ use std::fs;
698
+
699
+ use super::*;
700
+
701
+ const GPT2_REGEX: &str =
702
+ r"'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+";
703
+
704
+ /// Load the first `max_bytes` of ~/data/owt_train.txt, truncated to a UTF-8 boundary.
705
+ fn load_owt(max_bytes: usize) -> Vec<u8> {
706
+ let data_dir = std::env::home_dir().unwrap().join("data");
707
+ let all_bytes =
708
+ fs::read(data_dir.join("owt_train.txt")).expect("Could not read ~/data/owt_train.txt");
709
+ let mut end = max_bytes.min(all_bytes.len());
710
+ while end > 0 && std::str::from_utf8(&all_bytes[..end]).is_err() {
711
+ end -= 1;
712
+ }
713
+ all_bytes[..end].to_vec()
714
+ }
715
+
716
+ /// `safe_split_ranges` must produce boundaries that no pretoken crosses,
717
+ /// for every supported scheme: pretokenizing the ranges independently and
718
+ /// concatenating must equal pretokenizing the whole input in one pass.
719
+ #[test]
720
+ fn test_safe_split_ranges_pretoken_equivalent() {
721
+ let input = load_owt(2_000_000);
722
+
723
+ let ranges = safe_split_ranges(&input, 10_000, &[]);
724
+ assert!(ranges.len() > 100, "expected many splits, got {}", ranges.len());
725
+ // Ranges must cover the input contiguously.
726
+ assert_eq!(ranges.first().unwrap().start, 0);
727
+ assert_eq!(ranges.last().unwrap().end, input.len());
728
+ for w in ranges.windows(2) {
729
+ assert_eq!(w[0].end, w[1].start);
730
+ }
731
+
732
+ fn collect<'a, I: Iterator<Item = Pretoken<'a>>>(it: I) -> Vec<&'a [u8]> {
733
+ it.map(|p| p.0).collect()
734
+ }
735
+
736
+ macro_rules! check_scheme {
737
+ ($name:literal, $ctor:path) => {
738
+ let whole = collect($ctor(&input));
739
+ let split: Vec<&[u8]> = ranges
740
+ .iter()
741
+ .flat_map(|r| collect($ctor(&input[r.clone()])))
742
+ .collect();
743
+ assert_eq!(whole, split, "scheme {} differs across safe splits", $name);
744
+ };
745
+ }
746
+ check_scheme!("r50k", FastR50kPretokenizer::new);
747
+ check_scheme!("cl100k", FastCl100kPretokenizer::new);
748
+ check_scheme!("qwen2", FastQwen2Pretokenizer::new);
749
+ check_scheme!("qwen3_5", FastQwen35Pretokenizer::new);
750
+ check_scheme!("olmo3", FastOlmo3Pretokenizer::new);
751
+ check_scheme!("deepseek_v3", FastDeepSeekV3Pretokenizer::new);
752
+ }
753
+
754
+ /// Deterministic LCG word soup so word lengths vary and split probes
755
+ /// hit `special` (followed by `sep`) at every possible phase.
756
+ fn lcg_words(special: &[u8], sep: &[u8], period: usize) -> Vec<u8> {
757
+ let mut rng = 0x9e3779b97f4a7c15u64;
758
+ let mut next = move || {
759
+ rng = rng
760
+ .wrapping_mul(6364136223846793005)
761
+ .wrapping_add(1442695040888963407);
762
+ (rng >> 33) as usize
763
+ };
764
+ let words: [&[u8]; 5] = [b"alpha ", b"be ", b"gamma7 ", b"x ", b"delta "];
765
+ let mut input = Vec::new();
766
+ for _ in 0..4000 {
767
+ input.extend_from_slice(words[next() % words.len()]);
768
+ if next() % period == 0 {
769
+ input.extend_from_slice(special);
770
+ input.extend_from_slice(sep);
771
+ }
772
+ }
773
+ input
774
+ }
775
+
776
+ /// Boundaries must never cut an occurrence of a space-containing added
777
+ /// token, while splitting still proceeds elsewhere in the document.
778
+ #[test]
779
+ fn test_safe_split_ranges_avoids_added_tokens() {
780
+ let special: &[u8] = b"<|multi word special|>";
781
+ let input = lcg_words(special, b"", 9);
782
+
783
+ let ranges = safe_split_ranges(&input, 300, &[(special, false)]);
784
+ assert!(ranges.len() > 50, "expected many splits, got {}", ranges.len());
785
+ assert_eq!(ranges.first().unwrap().start, 0);
786
+ assert_eq!(ranges.last().unwrap().end, input.len());
787
+ for w in ranges.windows(2) {
788
+ assert_eq!(w[0].end, w[1].start);
789
+ }
790
+
791
+ let occurrences: Vec<usize> =
792
+ memchr::memmem::find_iter(&input, special).collect();
793
+ assert!(!occurrences.is_empty());
794
+ let cuts_occurrence = |p: usize| {
795
+ occurrences.iter().any(|&s| s < p && s + special.len() > p)
796
+ };
797
+ for r in &ranges[1..] {
798
+ assert!(!cuts_occurrence(r.start), "boundary {} cuts an occurrence", r.start);
799
+ }
800
+
801
+ // The input must actually tempt the splitter: without the
802
+ // added-token check, some boundary lands inside an occurrence.
803
+ let unaware = safe_split_ranges(&input, 300, &[]);
804
+ assert!(
805
+ unaware[1..].iter().any(|r| cuts_occurrence(r.start)),
806
+ "test input never places a naive boundary inside the special token"
807
+ );
808
+ }
809
+
810
+ /// A boundary must never sit right after an rstrip added-token
811
+ /// occurrence: the boundary space would open the next chunk as plain
812
+ /// text instead of being absorbed by the token's rstrip.
813
+ #[test]
814
+ fn test_safe_split_ranges_avoids_rstrip_token_ends() {
815
+ let special: &[u8] = b"TOK1";
816
+ let input = lcg_words(special, b" ", 6);
817
+ let ends_at = |p: usize| p >= special.len() && &input[p - special.len()..p] == special;
818
+
819
+ let ranges = safe_split_ranges(&input, 200, &[(special, true)]);
820
+ assert!(ranges.len() > 50, "expected many splits, got {}", ranges.len());
821
+ for r in &ranges[1..] {
822
+ assert!(!ends_at(r.start), "boundary {} follows an rstrip token", r.start);
823
+ }
824
+
825
+ // The input must actually tempt the splitter: without the rstrip
826
+ // flag, some boundary lands right after an occurrence.
827
+ let unaware = safe_split_ranges(&input, 200, &[(special, false)]);
828
+ assert!(
829
+ unaware[1..].iter().any(|r| ends_at(r.start)),
830
+ "test input never places a naive boundary after the rstrip token"
831
+ );
832
+ }
833
+
834
+ /// Compare the production (fast r50k) pretokenizer against the GPT-2
835
+ /// reference regex on ~5 MB of OWT data, token by token.
836
+ #[test]
837
+ fn test_pretokenizer_matches_regex_owt() {
838
+ const SIZE: usize = 5_000_000;
839
+ let input = load_owt(SIZE);
840
+ eprintln!(
841
+ "Testing pretokenizer vs regex on {:.1} MB of OWT",
842
+ input.len() as f64 / 1e6
843
+ );
844
+
845
+ let re = fancy_regex::Regex::new(GPT2_REGEX).unwrap();
846
+ let text = std::str::from_utf8(&input).unwrap();
847
+
848
+ let mut fast_iter = pretokenize_as_iter(&input);
849
+ let mut re_iter = re.find_iter(text);
850
+ let mut token_idx: usize = 0;
851
+ let mut recent: Vec<(String, String)> = Vec::new();
852
+
853
+ loop {
854
+ match (fast_iter.next(), re_iter.next()) {
855
+ (Some(fast_tok), Some(re_match)) => {
856
+ let re_match = re_match.expect("regex match error");
857
+ let fast_str = String::from_utf8_lossy(fast_tok.0);
858
+ let re_str = &text[re_match.start()..re_match.end()];
859
+ recent.push((fast_str.to_string(), re_str.to_string()));
860
+ if recent.len() > 10 {
861
+ recent.remove(0);
862
+ }
863
+ assert_eq!(
864
+ fast_str, re_str,
865
+ "Mismatch at token {token_idx} (byte ~{}).\n fast: {:?}\n regex: {:?}\n recent tokens: {:?}",
866
+ re_match.start(), fast_str, re_str, recent
867
+ );
868
+ }
869
+ (None, None) => break,
870
+ (Some(fast_tok), None) => {
871
+ panic!(
872
+ "Fast pretokenizer produced extra token at index {token_idx}: {:?}\n recent: {:?}",
873
+ String::from_utf8_lossy(fast_tok.0),
874
+ recent
875
+ );
876
+ }
877
+ (None, Some(re_match)) => {
878
+ let re_match = re_match.expect("regex match error");
879
+ panic!(
880
+ "Regex produced extra token at index {token_idx}: {:?}\n recent: {:?}",
881
+ &text[re_match.start()..re_match.end()],
882
+ recent
883
+ );
884
+ }
885
+ }
886
+ token_idx += 1;
887
+ }
888
+ eprintln!("All {token_idx} tokens match.");
889
+ }
890
+
891
+ #[test]
892
+ fn test_pretokenizer_ts() {
893
+ let data_dir = std::env::home_dir().unwrap().join("data");
894
+ let file_bytes = fs::read(data_dir.join("TinyStoriesV2-GPT4-train.txt")).unwrap();
895
+
896
+ let pretokenized_counts = pretokenize_as_iter(&file_bytes).counts();
897
+ eprintln!("Pretokenized {} unique tokens", pretokenized_counts.len());
898
+
899
+ let mut sorted_counts: Vec<_> = pretokenized_counts.iter().collect();
900
+ sorted_counts.sort_by_key(|&(_, &v)| v);
901
+ sorted_counts.reverse();
902
+ for &(&token, &count) in sorted_counts.iter().take(100) {
903
+ eprintln!("{1}: {0}", String::from_utf8_lossy(&token), count);
904
+ }
905
+ }
906
+
907
+ #[test]
908
+ fn test_pretokenizer_owt_length() {
909
+ let data_dir = std::env::home_dir().unwrap().join("data");
910
+ let file_bytes = fs::read(data_dir.join("owt_train.txt")).unwrap();
911
+
912
+ let pretokens_count = pretokenize_as_iter(&file_bytes).count();
913
+ eprintln!("Pretokenized {pretokens_count} tokens");
914
+ }
915
+ }
916
+
917
+ #[cfg(test)]
918
+ mod span_source_tests {
919
+ use super::*;
920
+
921
+ fn check_source<'a>(
922
+ mut src: impl PretokenSpans<'a>,
923
+ reference: impl Iterator<Item = Pretoken<'a>>,
924
+ scheme: &str,
925
+ ) {
926
+ let expected: Vec<&[u8]> = reference.map(|p| p.0).collect();
927
+ let mut got: Vec<&[u8]> = Vec::new();
928
+ let mut batch = SpanBatch::new();
929
+ let prefetched = std::cell::Cell::new(0usize);
930
+ loop {
931
+ let n = src.fill_spans_keyed(&mut batch, &|_h| {
932
+ prefetched.set(prefetched.get() + 1)
933
+ });
934
+ for i in 0..n {
935
+ // SAFETY: i < n, the count just returned by the fill.
936
+ let span = unsafe { batch.span(i) };
937
+ let (want_key, want_hash) = match pack_pretoken_key(span) {
938
+ Some(key) => (key, pretoken_key_hash(key)),
939
+ None => (0, 0),
940
+ };
941
+ let e = &batch.entries[i];
942
+ assert_eq!(e.key, want_key, "{scheme}: bad key for {span:?}");
943
+ let want_meta = if want_key != 0 {
944
+ want_hash
945
+ } else {
946
+ span.len() as u64
947
+ };
948
+ assert_eq!(e.meta, want_meta, "{scheme}: bad meta for {span:?}");
949
+ got.push(span);
950
+ }
951
+ if n < PRETOKEN_CHUNK {
952
+ break;
953
+ }
954
+ }
955
+ assert_eq!(prefetched.get(), got.len(), "{scheme}: one prefetch per span");
956
+ assert_eq!(
957
+ got.len(),
958
+ expected.len(),
959
+ "{scheme}: span count mismatch"
960
+ );
961
+ for (i, (g, e)) in got.iter().zip(&expected).enumerate() {
962
+ assert_eq!(
963
+ g, e,
964
+ "{scheme}: span {i} diverged: {:?} vs {:?}",
965
+ String::from_utf8_lossy(g),
966
+ String::from_utf8_lossy(e)
967
+ );
968
+ }
969
+ }
970
+
971
+ /// [`check_source`] for every mask-scanner scheme on `b`.
972
+ fn check_all_mask_schemes(b: &[u8]) {
973
+ check_source(FastR50kPretokenizer::new(b), FastR50kPretokenizer::new(b), "r50k");
974
+ check_source(
975
+ FastCl100kPretokenizer::new(b),
976
+ FastCl100kPretokenizer::new(b),
977
+ "cl100k",
978
+ );
979
+ check_source(FastQwen2Pretokenizer::new(b), FastQwen2Pretokenizer::new(b), "qwen2");
980
+ check_source(
981
+ FastQwen35Pretokenizer::new(b),
982
+ FastQwen35Pretokenizer::new(b),
983
+ "qwen3_5",
984
+ );
985
+ check_source(FastOlmo3Pretokenizer::new(b), FastOlmo3Pretokenizer::new(b), "olmo3");
986
+ check_source(
987
+ FastDeepSeekV3Pretokenizer::new(b),
988
+ FastDeepSeekV3Pretokenizer::new(b),
989
+ "deepseek_v3",
990
+ );
991
+ }
992
+
993
+ /// Every scheme's chunked `fill_spans_keyed` must reproduce its
994
+ /// iterator's spans exactly, with keys/hashes derived per the shared
995
+ /// helpers, one prefetch per span — including chunk-boundary and
996
+ /// end-of-input handling (buffer sizes straddle multiples of
997
+ /// PRETOKEN_CHUNK spans).
998
+ #[test]
999
+ fn fill_spans_keyed_matches_iterator_all_schemes() {
1000
+ let pieces: &[&str] = &[
1001
+ "word", " word", "12", " 345678", "'ll", "'s", " ", " ", "\n", "\r\n", "\t",
1002
+ "!", " ?!", "(", "caf\u{e9}", "\u{65e5}\u{672c}", "\u{1F389}", "\u{00A0}",
1003
+ "\u{2003}", "a", " I", "don't", "one two three four five", "\u{4e2d}\u{6587}",
1004
+ "\u{30ab}\u{30bf}", "123456", " longpretokenword", "supercalifragilistic",
1005
+ ];
1006
+ let mut state = 0x9E3779B97F4A7C15u64;
1007
+ let mut rng = move || {
1008
+ state ^= state << 13;
1009
+ state ^= state >> 7;
1010
+ state ^= state << 17;
1011
+ state
1012
+ };
1013
+ for round in 0..60 {
1014
+ // Vary length so end-of-input lands at many chunk offsets.
1015
+ let target = 40 + round * 97;
1016
+ let mut buf = Vec::new();
1017
+ while buf.len() < target {
1018
+ buf.extend_from_slice(pieces[(rng() % pieces.len() as u64) as usize].as_bytes());
1019
+ }
1020
+ let b: &[u8] = &buf;
1021
+ check_all_mask_schemes(b);
1022
+ check_source(
1023
+ SpanIter(PretokenizerIter::new(b)),
1024
+ PretokenizerIter::new(b),
1025
+ "state_machine",
1026
+ );
1027
+ for pt in [
1028
+ PretokenizerType::GPT2,
1029
+ PretokenizerType::GPT4,
1030
+ PretokenizerType::Qwen2,
1031
+ PretokenizerType::Qwen35,
1032
+ PretokenizerType::Olmo3,
1033
+ PretokenizerType::DeepSeekV3,
1034
+ PretokenizerType::Kimi,
1035
+ ] {
1036
+ check_source(pt.pretokenize(b), pt.pretokenize(b), "dispatch");
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ /// Long runs hit the two-phase walker's rare paths: a pretoken longer
1042
+ /// than its u16 offset window (direct single-span emit, both the
1043
+ /// clean-batch and bad-zone variants), a scalar overrun whose end
1044
+ /// leaves the window mid-fill (dropped and re-derived next fill), and
1045
+ /// maximum-density boundary batches straddling fill boundaries.
1046
+ #[test]
1047
+ fn fill_spans_keyed_long_runs_all_schemes() {
1048
+ let mut cases: Vec<Vec<u8>> = Vec::new();
1049
+ // > 64 KB letter run between ordinary words.
1050
+ let mut v = b"intro words ".to_vec();
1051
+ v.extend(std::iter::repeat_n(b'a', 70_000));
1052
+ v.extend_from_slice(b" tail words");
1053
+ cases.push(v);
1054
+ // > 64 KB space run: boundaries only at the run's edges.
1055
+ let mut v = b"x".to_vec();
1056
+ v.extend(std::iter::repeat_n(b' ', 70_000));
1057
+ v.extend_from_slice(b"y z");
1058
+ cases.push(v);
1059
+ // > 64 KB of 3-byte unicode whitespace: batch-edge straddles make
1060
+ // bad zones, and the scalar walk overruns the whole run.
1061
+ let mut v = b"hello world ".repeat(4);
1062
+ for _ in 0..25_000 {
1063
+ v.extend_from_slice("\u{2003}".as_bytes());
1064
+ }
1065
+ v.extend_from_slice(b"end");
1066
+ cases.push(v);
1067
+ // Monster token flush at end of input.
1068
+ cases.push(std::iter::repeat_n(b'a', 70_000).collect());
1069
+ // Single-byte token alternation (64 boundaries per batch), then a
1070
+ // digit run some schemes split every 3 chars.
1071
+ let mut v = b"a1".repeat(2_000);
1072
+ v.extend(std::iter::repeat_n(b'7', 40_000));
1073
+ v.extend_from_slice(b" end");
1074
+ cases.push(v);
1075
+ for case in &cases {
1076
+ check_all_mask_schemes(case);
1077
+ }
1078
+ }
1079
+ }