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,1486 @@
1
+ //! Shared infrastructure for mask-scanner pretokenizers.
2
+ //!
3
+ //! A mask-scanner pretokenizer processes 64-byte batches: SIMD classifies
4
+ //! every byte, bitmask algebra derives "a token starts here" bits, and
5
+ //! `next()` pops one bit per token — no per-token dispatch branches, which
6
+ //! is what makes it ~2x the serial scalar scanners (see
7
+ //! pretokenizer_optimization_log.md step 15).
8
+ //!
9
+ //! A scheme plugs in two functions ([`MaskScheme`]):
10
+ //! - `advance`: the scalar ground truth (also the no-SIMD iterator),
11
+ //! - `batch_masks`: `(usable, bad)` bitmasks for a 64-byte batch. `usable`
12
+ //! bits are trustworthy token starts; `bad` marks zones (non-ASCII the
13
+ //! scheme doesn't classify in-mask, batch-edge ambiguities) that
14
+ //! [`MaskState`] re-derives through `advance`, never emitting a token
15
+ //! across an unresolved zone.
16
+ //!
17
+ //! Layering, bottom to top:
18
+ //! 1. Platform SIMD primitives (`movemask64`, `ascii_masks` on NEON;
19
+ //! `ascii_masks_avx512` / `ascii_masks_avx2` on x86-64) — the only
20
+ //! per-platform code.
21
+ //! 2. Bit-domain helpers shared across schemes — platform-independent
22
+ //! u64 algebra and per-char table classification
23
+ //! (`classify_uni_chars`, `char_through`, `nn_at_full`,
24
+ //! `digit_run_splits3`), parameterized by each scheme's codepoint
25
+ //! classifier.
26
+ //! 3. Per-scheme `batch_masks` boundary algebra (in the scheme's module).
27
+ //! 4. [`MaskState`] — the scheme-agnostic batch walker: segments, bad-zone
28
+ //! gaps, scalar tail, one-batch-ahead precompute; scalar overruns stay
29
+ //! on the 64-byte grid so the precompute survives them.
30
+ //! 5. [`MaskState::fill_spans_two_phase`] — the chunked pull the encode
31
+ //! loop uses: the same masks and trust rules as `next_span`, but
32
+ //! harvested a chunk at a time into a flat boundary buffer and emitted
33
+ //! in a branch-free counted loop.
34
+
35
+ use crate::pretokenize::unicode::{self, CharClass};
36
+
37
+ // -----------------------------------------------------------------------
38
+ // Platform SIMD primitives: aarch64 NEON (compile-time, always present)
39
+ // and x86_64 AVX-512 or AVX2 (runtime-detected; scalar fallback
40
+ // otherwise).
41
+ // -----------------------------------------------------------------------
42
+
43
+ /// Does this x86_64 CPU have the full AVX-512 tier (Zen 4/5, Ice
44
+ /// Lake+)? Schemes dispatch their batch classifier on this: the AVX-512
45
+ /// front-end when true, the AVX2 one otherwise.
46
+ #[cfg(target_arch = "x86_64")]
47
+ #[inline]
48
+ pub(crate) fn avx512_scanner_available() -> bool {
49
+ // std's feature cache makes this an atomic load + bit test after the
50
+ // first call.
51
+ std::arch::is_x86_feature_detected!("avx512f")
52
+ && std::arch::is_x86_feature_detected!("avx512bw")
53
+ && std::arch::is_x86_feature_detected!("avx512vl")
54
+ && std::arch::is_x86_feature_detected!("bmi1")
55
+ && std::arch::is_x86_feature_detected!("bmi2")
56
+ && std::arch::is_x86_feature_detected!("lzcnt")
57
+ && std::arch::is_x86_feature_detected!("popcnt")
58
+ }
59
+
60
+ /// Does this x86_64 CPU also have AVX-512 VBMI2 (native 512-bit
61
+ /// `vpcompressb`: Zen 4/5, Ice Lake+ — i.e. nearly every AVX-512 CPU,
62
+ /// but the bit is detected, not assumed: Skylake-X lacks it and stays on
63
+ /// the plain AVX-512 tier)? Gates the `X86_TIER_AVX512_VBMI2` fill tier
64
+ /// ([`MaskState::fill_spans_two_phase`]'s `_avx512_vbmi2_crc` wrapper),
65
+ /// whose `flatten_bits_avx512` needs VBMI2 on top of the scanner tier.
66
+ #[cfg(target_arch = "x86_64")]
67
+ #[inline]
68
+ pub(crate) fn avx512_fill_available() -> bool {
69
+ avx512_scanner_available() && std::arch::is_x86_feature_detected!("avx512vbmi2")
70
+ }
71
+
72
+ /// Does this x86_64 CPU have the AVX2 tier (Haswell+, all Zen)? The bit
73
+ /// features (BMI1/2, LZCNT, POPCNT) arrived with or before AVX2 on every
74
+ /// AVX2 CPU, but are detected explicitly since the boundary algebra's
75
+ /// codegen relies on them.
76
+ #[cfg(target_arch = "x86_64")]
77
+ #[inline]
78
+ pub(crate) fn avx2_scanner_available() -> bool {
79
+ std::arch::is_x86_feature_detected!("avx2")
80
+ && std::arch::is_x86_feature_detected!("bmi1")
81
+ && std::arch::is_x86_feature_detected!("bmi2")
82
+ && std::arch::is_x86_feature_detected!("lzcnt")
83
+ && std::arch::is_x86_feature_detected!("popcnt")
84
+ }
85
+
86
+ /// Is the SIMD mask scanner usable on this machine? aarch64 always has
87
+ /// NEON; x86_64 requires AVX-512 (Zen 4/5, Ice Lake+) or AVX2 (Haswell+,
88
+ /// Zen 1-3), detected at runtime. When this returns false, [`MaskState`]
89
+ /// runs every token through the scheme's scalar `advance`.
90
+ #[cfg(target_arch = "x86_64")]
91
+ #[inline]
92
+ pub(crate) fn simd_scanner_available() -> bool {
93
+ avx512_scanner_available() || avx2_scanner_available()
94
+ }
95
+
96
+ #[cfg(not(target_arch = "x86_64"))]
97
+ #[inline]
98
+ pub(crate) fn simd_scanner_available() -> bool {
99
+ cfg!(target_arch = "aarch64")
100
+ }
101
+
102
+ // The x86-64 batch classifiers are annotated
103
+ // `#[target_feature(enable = "avx512f,avx512bw,avx512vl,bmi1,bmi2,lzcnt,popcnt")]`
104
+ // (AVX-512 tier) or `#[target_feature(enable = "avx2,bmi1,bmi2,lzcnt,popcnt")]`
105
+ // (AVX2 tier). Besides the wide byte ops, the scalar-visible bit features
106
+ // (BMI1/2, LZCNT, POPCNT) are enabled so the boundary algebra inlined
107
+ // into those functions compiles to tzcnt/lzcnt/blsr instead of
108
+ // baseline-x86 bsf sequences. The sets must stay in sync with
109
+ // [`avx512_scanner_available`] / [`avx2_scanner_available`].
110
+
111
+ /// simdjson-style movemask: 4 mask vectors (64 lanes of 0x00/0xFF) -> u64,
112
+ /// bit i = lane i.
113
+ #[cfg(target_arch = "aarch64")]
114
+ #[inline(always)]
115
+ pub(crate) unsafe fn movemask64(
116
+ v0: std::arch::aarch64::uint8x16_t,
117
+ v1: std::arch::aarch64::uint8x16_t,
118
+ v2: std::arch::aarch64::uint8x16_t,
119
+ v3: std::arch::aarch64::uint8x16_t,
120
+ ) -> u64 {
121
+ use std::arch::aarch64::*;
122
+ unsafe {
123
+ const W: [u8; 16] = [1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128];
124
+ let w = vld1q_u8(W.as_ptr());
125
+ let mut a0 = vandq_u8(v0, w);
126
+ let a1 = vandq_u8(v1, w);
127
+ let mut a2 = vandq_u8(v2, w);
128
+ let a3 = vandq_u8(v3, w);
129
+ // The 4-`addp` reduction tree (simdjson's arm64 movemask), pinned
130
+ // as asm. Written with `vpaddq_u8`, LLVM rewrites every pairwise
131
+ // add into a uzp1/uzp2/orr triple — adjacent weighted lanes have
132
+ // disjoint bits, so add == or, and the canonical or-form never
133
+ // re-forms addp — inflating each call from 9 to 17 vector ops
134
+ // (4-7 calls per 64-byte batch across the schemes). The weighted
135
+ // `and`s stay outside so the scheduler still interleaves
136
+ // neighboring calls. `addp(x, x)` lane 0..7 equals the old
137
+ // `addp(x, zero)` lanes 0..7; only lane u64 0 is read.
138
+ core::arch::asm!(
139
+ "addp {a0:v}.16b, {a0:v}.16b, {a1:v}.16b",
140
+ "addp {a2:v}.16b, {a2:v}.16b, {a3:v}.16b",
141
+ "addp {a0:v}.16b, {a0:v}.16b, {a2:v}.16b",
142
+ "addp {a0:v}.16b, {a0:v}.16b, {a0:v}.16b",
143
+ a0 = inout(vreg) a0,
144
+ a1 = in(vreg) a1,
145
+ a2 = inout(vreg) a2,
146
+ a3 = in(vreg) a3,
147
+ options(pure, nomem, nostack, preserves_flags),
148
+ );
149
+ vgetq_lane_u64::<0>(vreinterpretq_u64_u8(a0))
150
+ }
151
+ }
152
+
153
+ /// One u64 mask (bit i = byte scan+i) per byte predicate, for 64 bytes.
154
+ /// The working currency of scheme boundary algebra: everything after this
155
+ /// is platform-independent u64 bit math.
156
+ #[derive(Clone, Copy, Default)]
157
+ pub(crate) struct AsciiMasks {
158
+ /// ASCII letters.
159
+ pub l: u64,
160
+ /// ASCII digits.
161
+ pub d: u64,
162
+ /// Space (0x20) only.
163
+ pub s: u64,
164
+ /// Non-newline ASCII whitespace: \t, \x0b, \x0c.
165
+ pub wt: u64,
166
+ /// Newlines: \r, \n.
167
+ pub n: u64,
168
+ /// Non-ASCII bytes (>= 0x80).
169
+ pub hi: u64,
170
+ /// ASCII apostrophes.
171
+ pub ap: u64,
172
+ }
173
+
174
+ /// Classify `bytes[scan..scan+64]` (requires `scan + 64 <= bytes.len()`).
175
+ #[cfg(target_arch = "aarch64")]
176
+ #[inline(always)]
177
+ pub(crate) fn ascii_masks(bytes: &[u8], scan: usize) -> AsciiMasks {
178
+ use std::arch::aarch64::*;
179
+ unsafe {
180
+ let p = bytes.as_ptr().add(scan);
181
+ let mut l = [vdupq_n_u8(0); 4];
182
+ let mut d = [vdupq_n_u8(0); 4];
183
+ let mut s = [vdupq_n_u8(0); 4];
184
+ let mut wt = [vdupq_n_u8(0); 4];
185
+ let mut n = [vdupq_n_u8(0); 4];
186
+ let mut hi = [vdupq_n_u8(0); 4];
187
+ let mut ap = [vdupq_n_u8(0); 4];
188
+ for i in 0..4 {
189
+ let v = vld1q_u8(p.add(16 * i));
190
+ let lowered = vorrq_u8(v, vdupq_n_u8(0x20));
191
+ l[i] = vcleq_u8(vsubq_u8(lowered, vdupq_n_u8(b'a')), vdupq_n_u8(25));
192
+ d[i] = vcleq_u8(vsubq_u8(v, vdupq_n_u8(b'0')), vdupq_n_u8(9));
193
+ s[i] = vceqq_u8(v, vdupq_n_u8(b' '));
194
+ n[i] = vorrq_u8(
195
+ vceqq_u8(v, vdupq_n_u8(b'\r')),
196
+ vceqq_u8(v, vdupq_n_u8(b'\n')),
197
+ );
198
+ // \t (9), \x0b (11), \x0c (12): ascii ws minus \r\n and space.
199
+ wt[i] = vbicq_u8(
200
+ vcleq_u8(vsubq_u8(v, vdupq_n_u8(9)), vdupq_n_u8(4)),
201
+ n[i],
202
+ );
203
+ hi[i] = vcltzq_s8(vreinterpretq_s8_u8(v));
204
+ ap[i] = vceqq_u8(v, vdupq_n_u8(b'\''));
205
+ }
206
+ AsciiMasks {
207
+ l: movemask64(l[0], l[1], l[2], l[3]),
208
+ d: movemask64(d[0], d[1], d[2], d[3]),
209
+ s: movemask64(s[0], s[1], s[2], s[3]),
210
+ wt: movemask64(wt[0], wt[1], wt[2], wt[3]),
211
+ n: movemask64(n[0], n[1], n[2], n[3]),
212
+ hi: movemask64(hi[0], hi[1], hi[2], hi[3]),
213
+ ap: movemask64(ap[0], ap[1], ap[2], ap[3]),
214
+ }
215
+ }
216
+ }
217
+
218
+ /// Classify `bytes[scan..scan+64]` with AVX-512 (requires
219
+ /// `scan + 64 <= bytes.len()`). One 64-byte load and one k-register
220
+ /// compare per predicate: a `__mmask64` IS the u64 the bit algebra wants,
221
+ /// so there is no movemask ladder and no lazy any-tests — every field
222
+ /// (including `hi` and `ap`) is computed unconditionally.
223
+ ///
224
+ /// Runtime-gated: callers reach this only after
225
+ /// [`simd_scanner_available`] reported AVX-512 support (enforced by
226
+ /// [`MaskState`], which otherwise never leaves the scalar path).
227
+ #[cfg(target_arch = "x86_64")]
228
+ #[target_feature(enable = "avx512f,avx512bw,avx512vl,bmi1,bmi2,lzcnt,popcnt")]
229
+ #[inline]
230
+ pub(crate) fn ascii_masks_avx512(bytes: &[u8], scan: usize) -> AsciiMasks {
231
+ use std::arch::x86_64::*;
232
+ unsafe {
233
+ let v = _mm512_loadu_si512(bytes.as_ptr().add(scan) as *const _);
234
+ let lowered = _mm512_or_si512(v, _mm512_set1_epi8(0x20));
235
+ let l = _mm512_cmple_epu8_mask(
236
+ _mm512_sub_epi8(lowered, _mm512_set1_epi8(b'a' as i8)),
237
+ _mm512_set1_epi8(25),
238
+ );
239
+ let d = _mm512_cmple_epu8_mask(
240
+ _mm512_sub_epi8(v, _mm512_set1_epi8(b'0' as i8)),
241
+ _mm512_set1_epi8(9),
242
+ );
243
+ let s = _mm512_cmpeq_epi8_mask(v, _mm512_set1_epi8(b' ' as i8));
244
+ let n = _mm512_cmpeq_epi8_mask(v, _mm512_set1_epi8(b'\r' as i8))
245
+ | _mm512_cmpeq_epi8_mask(v, _mm512_set1_epi8(b'\n' as i8));
246
+ // \t (9), \x0b (11), \x0c (12): ascii ws minus \r\n and space.
247
+ let wt = _mm512_cmple_epu8_mask(
248
+ _mm512_sub_epi8(v, _mm512_set1_epi8(9)),
249
+ _mm512_set1_epi8(4),
250
+ ) & !n;
251
+ let hi = _mm512_movepi8_mask(v) as u64;
252
+ let ap = _mm512_cmpeq_epi8_mask(v, _mm512_set1_epi8(b'\'' as i8));
253
+ AsciiMasks { l, d, s, wt, n, hi, ap }
254
+ }
255
+ }
256
+
257
+ /// Classify `bytes[scan..scan+64]` with AVX2 (requires
258
+ /// `scan + 64 <= bytes.len()`). Two 32-byte loads; each predicate is one
259
+ /// vector compare per half plus a `vpmovmskb` ladder into the u64 the bit
260
+ /// algebra wants — more mask-extraction traffic than the AVX-512 version
261
+ /// (whose k-register compares ARE the u64s), but the output currency is
262
+ /// identical, so everything downstream is shared. AVX2 has no unsigned
263
+ /// byte compare; `x <= lim` is `min_epu8(x, lim) == x`.
264
+ ///
265
+ /// Runtime-gated: callers reach this only after
266
+ /// [`avx2_scanner_available`] reported AVX2 support (enforced by the
267
+ /// schemes' dispatch, behind [`MaskState`]'s `simd_scanner_available`
268
+ /// gate).
269
+ ///
270
+ /// `#[inline(never)]` is load-bearing: inlined, LLVM's vector combiner
271
+ /// sees the compare vectors behind the returned u64s and pulls the
272
+ /// caller's scalar boundary algebra back into the byte-vector domain,
273
+ /// expanding every mask<->vector crossing into vpinsrb/vpextrb ladders
274
+ /// (~240 byte ops per batch, measured 3.5x slower end to end on Zen 2).
275
+ /// The AVX-512 tier has no such domain to return to (k-register compares
276
+ /// ARE the u64s), so it stays inline.
277
+ #[cfg(target_arch = "x86_64")]
278
+ #[target_feature(enable = "avx2,bmi1,bmi2,lzcnt,popcnt")]
279
+ #[inline(never)]
280
+ pub(crate) fn ascii_masks_avx2(bytes: &[u8], scan: usize) -> AsciiMasks {
281
+ use std::arch::x86_64::*;
282
+ unsafe {
283
+ // Closures inherit the enclosing fn's target features.
284
+ let le = |v: __m256i, lim: __m256i| -> __m256i {
285
+ _mm256_cmpeq_epi8(_mm256_min_epu8(v, lim), v)
286
+ };
287
+ let mm = |m0: __m256i, m1: __m256i| -> u64 {
288
+ (_mm256_movemask_epi8(m0) as u32 as u64)
289
+ | ((_mm256_movemask_epi8(m1) as u32 as u64) << 32)
290
+ };
291
+
292
+ let p = bytes.as_ptr().add(scan);
293
+ let v0 = _mm256_loadu_si256(p as *const _);
294
+ let v1 = _mm256_loadu_si256(p.add(32) as *const _);
295
+
296
+ let x20 = _mm256_set1_epi8(0x20);
297
+ let ca = _mm256_set1_epi8(b'a' as i8);
298
+ let c25 = _mm256_set1_epi8(25);
299
+ let l = mm(
300
+ le(_mm256_sub_epi8(_mm256_or_si256(v0, x20), ca), c25),
301
+ le(_mm256_sub_epi8(_mm256_or_si256(v1, x20), ca), c25),
302
+ );
303
+ let c0 = _mm256_set1_epi8(b'0' as i8);
304
+ let c9 = _mm256_set1_epi8(9);
305
+ let d = mm(
306
+ le(_mm256_sub_epi8(v0, c0), c9),
307
+ le(_mm256_sub_epi8(v1, c0), c9),
308
+ );
309
+ let sp = _mm256_set1_epi8(b' ' as i8);
310
+ let s = mm(_mm256_cmpeq_epi8(v0, sp), _mm256_cmpeq_epi8(v1, sp));
311
+ let cr = _mm256_set1_epi8(b'\r' as i8);
312
+ let lf = _mm256_set1_epi8(b'\n' as i8);
313
+ let n = mm(
314
+ _mm256_or_si256(_mm256_cmpeq_epi8(v0, cr), _mm256_cmpeq_epi8(v0, lf)),
315
+ _mm256_or_si256(_mm256_cmpeq_epi8(v1, cr), _mm256_cmpeq_epi8(v1, lf)),
316
+ );
317
+ // \t (9), \x0b (11), \x0c (12): ascii ws minus \r\n and space.
318
+ let c4 = _mm256_set1_epi8(4);
319
+ let wt = mm(
320
+ le(_mm256_sub_epi8(v0, c9), c4),
321
+ le(_mm256_sub_epi8(v1, c9), c4),
322
+ ) & !n;
323
+ let hi = mm(v0, v1); // vpmovmskb takes the sign bit directly
324
+ let apc = _mm256_set1_epi8(b'\'' as i8);
325
+ let ap = mm(_mm256_cmpeq_epi8(v0, apc), _mm256_cmpeq_epi8(v1, apc));
326
+ AsciiMasks { l, d, s, wt, n, hi, ap }
327
+ }
328
+ }
329
+
330
+ // -----------------------------------------------------------------------
331
+ // Bit-domain helpers (platform-independent)
332
+ // -----------------------------------------------------------------------
333
+
334
+ /// Is the char starting at `idx` NOT whitespace (`\S` for a `(?!\S)`
335
+ /// lookahead)? Full answer via the packed table.
336
+ ///
337
+ /// # Safety
338
+ ///
339
+ /// `idx < bytes.len()`, and when `bytes[idx]` is non-ASCII,
340
+ /// `idx + 4 <= bytes.len()` (the guardless [`decode_cp_inbounds`] read).
341
+ /// The batch classifiers' `scan + 70 <= len` guard covers every call
342
+ /// site's worst case (`idx = scan + 64`).
343
+ #[inline(always)]
344
+ pub(crate) unsafe fn nn_at_full(bytes: &[u8], idx: usize) -> bool {
345
+ use super::{decode_cp_inbounds, is_ascii_ws};
346
+ let b = bytes[idx];
347
+ if b < 0x80 {
348
+ return !is_ascii_ws(b);
349
+ }
350
+ // SAFETY: caller guarantees idx + 4 <= len for a non-ASCII byte here
351
+ // (this fn's contract).
352
+ let (cp, _) = unsafe { decode_cp_inbounds(bytes, idx) };
353
+ unicode::class_of(cp) != CharClass::Whitespace
354
+ }
355
+
356
+ /// The char containing byte `pos - 1` (`pos > 0`, valid UTF-8): its
357
+ /// class, lead index, and end (exclusive). `end > pos` iff the char
358
+ /// straddles across `pos`. ASCII classifies with the byte predicates;
359
+ /// multi-byte chars walk back to their lead (at most 3 bytes) and use
360
+ /// the packed table — this is what lets a batch after a unicode char
361
+ /// compute true boundary carries instead of deferring to a bad zone.
362
+ /// `class`: the scheme's codepoint classifier (`unicode::class_of`, or a
363
+ /// mark-folding view like `unicode::class_of_marks_join`).
364
+ ///
365
+ /// # Safety
366
+ ///
367
+ /// `pos > 0`, and when `bytes[pos - 1]` is non-ASCII,
368
+ /// `pos + 3 <= bytes.len()`: the walk-back lead `j` satisfies
369
+ /// `j <= pos - 1`, so the guardless [`decode_cp_inbounds`] read needs
370
+ /// `j + 4 <= pos + 3` in-bounds bytes. The batch classifiers' `scan + 70
371
+ /// <= len` guard covers every call site (`pos <= scan + 64`).
372
+ #[inline(always)]
373
+ pub(crate) unsafe fn char_through(
374
+ bytes: &[u8],
375
+ pos: usize,
376
+ class: impl Fn(u32) -> CharClass,
377
+ ) -> (CharClass, usize, usize) {
378
+ use super::{decode_cp_inbounds, is_ascii_ws, is_digit, is_letter};
379
+ let b = bytes[pos - 1];
380
+ if b < 0x80 {
381
+ let cls = if is_letter(b) {
382
+ CharClass::Letter
383
+ } else if is_digit(b) {
384
+ CharClass::Number
385
+ } else if is_ascii_ws(b) {
386
+ CharClass::Whitespace
387
+ } else {
388
+ CharClass::Other
389
+ };
390
+ return (cls, pos - 1, pos);
391
+ }
392
+ let mut j = pos - 1;
393
+ while j > 0 && bytes[j] & 0xC0 == 0x80 {
394
+ j -= 1;
395
+ }
396
+ // SAFETY: j < pos and pos + 3 <= len (this fn's contract), so
397
+ // j + 4 <= len.
398
+ let (cp, l) = unsafe { decode_cp_inbounds(bytes, j) };
399
+ (class(cp), j, j + l)
400
+ }
401
+
402
+ /// Per-byte class masks for a batch's unicode chars, classified with the
403
+ /// packed table (`unicode::class_of`) — the same lookup the scalar paths
404
+ /// do. Every byte of a classified char carries the char's class, so
405
+ /// byte-adjacency == char-adjacency and the schemes' u64 boundary
406
+ /// algebra applies unchanged.
407
+ #[derive(Clone, Copy, Default)]
408
+ pub(crate) struct UniClasses {
409
+ /// Letter / number / other / whitespace bytes.
410
+ pub l: u64,
411
+ pub n: u64,
412
+ pub o: u64,
413
+ pub ws: u64,
414
+ /// Whitespace lead bits by char length, for the char-length-aware
415
+ /// `(?!\S)` shift tests. Deferred ws chars (see `resid`) are not
416
+ /// included.
417
+ pub w2: u64,
418
+ pub w3: u64,
419
+ /// Lead bits of all classified chars by length, for schemes that
420
+ /// shift a test by the previous char's length (the cl100k family's
421
+ /// two-chars-back rule).
422
+ pub lead2: u64,
423
+ pub lead3: u64,
424
+ pub lead4: u64,
425
+ /// Continuation bytes of classified chars.
426
+ pub cont: u64,
427
+ /// Bytes only the scalar path can decide: whitespace chars straddling
428
+ /// the batch end (their run-split bookkeeping crosses the boundary),
429
+ /// number chars when `NUMBERS` is false, and stray continuation
430
+ /// bytes. Class masks stay truthful for these bytes so neighbors'
431
+ /// algebra is exact; callers turn `resid` into bad zones (±1 smear).
432
+ pub resid: u64,
433
+ }
434
+
435
+ /// Classify every unicode char whose lead bit is in `m` (typically
436
+ /// `hi & !claimed-straddle-in-bytes`) for `bytes[scan..scan+64]`.
437
+ /// A char spilling off the batch end is classified via the lookahead
438
+ /// bytes; only its in-batch bytes get class bits, and the next
439
+ /// batch's `char_through` walk-back covers the remainder. `NUMBERS`:
440
+ /// false for schemes whose digit grouping is char-counted (`\p{N}{1,3}`
441
+ /// byte masks can't express multi-byte chars), true otherwise.
442
+ /// `LEADS`: whether to fill the per-length lead masks (only schemes with
443
+ /// a shift-by-prev-char-length rule need them).
444
+ ///
445
+ /// The loop stays branchy on purpose: a branchless csel-selected
446
+ /// decode/classify body measured 0.986x (predicted branches beat data
447
+ /// chains, log step 13/17). 2-byte chars (nearly all non-ASCII in western
448
+ /// corpora) take a dedicated lane with an inline decode; 3/4-byte chars
449
+ /// pay the general ladder.
450
+ ///
451
+ /// # Safety
452
+ ///
453
+ /// `scan + 70 <= bytes.len()` (the batch classifiers' lookahead guard):
454
+ /// a lead bit at position 63 puts the guardless [`decode_cp_inbounds`]
455
+ /// read at `scan + 63`, which may touch through `scan + 67`.
456
+ #[inline(always)]
457
+ pub(crate) unsafe fn classify_uni_chars<const NUMBERS: bool, const LEADS: bool>(
458
+ bytes: &[u8],
459
+ scan: usize,
460
+ mut m: u64,
461
+ class: impl Fn(u32) -> CharClass,
462
+ ) -> UniClasses {
463
+ use super::decode_cp_inbounds;
464
+ let mut u = UniClasses::default();
465
+ while m != 0 {
466
+ let i = m.trailing_zeros() as usize;
467
+ m &= m - 1;
468
+ let b = bytes[scan + i];
469
+ if b < 0xE0 {
470
+ // 2-byte lane (leads 0xC2..0xDF, cp < 0x800): nearly every
471
+ // non-ASCII char in western corpora, so this branch predicts
472
+ // taken and skips the length ladder + general decode.
473
+ if b < 0xC2 {
474
+ u.resid |= 1 << i; // stray continuation byte (invalid UTF-8)
475
+ continue;
476
+ }
477
+ let lead = 1u64 << i;
478
+ let chm = 3u64 << i; // in-batch bytes (excess drops at bit 63)
479
+ // SAFETY: scan + 70 <= len (this fn's # Safety contract),
480
+ // i <= 63, so scan + i + 1 <= scan + 64 < len.
481
+ let b1 = unsafe { *bytes.get_unchecked(scan + i + 1) };
482
+ let cp = ((b as u32 & 0x1F) << 6) | (b1 as u32 & 0x3F);
483
+ match class(cp) {
484
+ CharClass::Letter => u.l |= chm,
485
+ CharClass::Number => {
486
+ u.n |= chm;
487
+ if !NUMBERS {
488
+ u.resid |= chm;
489
+ }
490
+ }
491
+ CharClass::Other => u.o |= chm,
492
+ CharClass::Whitespace => {
493
+ u.ws |= chm;
494
+ if i + 2 > 64 {
495
+ // Straddling-out ws stays a bad zone; its true
496
+ // class marks keep neighbors' `(?!\S)` tests
497
+ // exact.
498
+ u.resid |= chm;
499
+ } else {
500
+ u.w2 |= lead;
501
+ }
502
+ }
503
+ }
504
+ if LEADS {
505
+ u.lead2 |= lead;
506
+ }
507
+ u.cont |= chm & !lead;
508
+ m &= !chm;
509
+ continue;
510
+ }
511
+ let l = if b < 0xF0 { 3 } else { 4 };
512
+ let chm = ((1u64 << l) - 1) << i; // in-batch bytes (excess drops)
513
+ let lead = 1u64 << i;
514
+ // SAFETY: scan + 70 <= len (this fn's # Safety contract), i <= 63,
515
+ // so scan + i + 4 <= len even for a 4-byte lead at bit 63.
516
+ let (cp, _) = unsafe { decode_cp_inbounds(bytes, scan + i) };
517
+ match class(cp) {
518
+ CharClass::Letter => u.l |= chm,
519
+ CharClass::Number => {
520
+ u.n |= chm;
521
+ if !NUMBERS {
522
+ u.resid |= chm;
523
+ }
524
+ }
525
+ CharClass::Other => u.o |= chm,
526
+ CharClass::Whitespace => {
527
+ u.ws |= chm;
528
+ if i + l > 64 || l == 4 {
529
+ // Straddling-out ws (and defensively: no 4-byte cp
530
+ // is ws in Unicode) stays a bad zone; its true class
531
+ // marks keep neighbors' `(?!\S)` tests exact.
532
+ u.resid |= chm;
533
+ } else {
534
+ u.w3 |= lead;
535
+ }
536
+ }
537
+ }
538
+ if LEADS {
539
+ if l == 3 {
540
+ u.lead3 |= lead;
541
+ } else {
542
+ u.lead4 |= lead;
543
+ }
544
+ }
545
+ u.cont |= chm & !lead;
546
+ m &= !chm;
547
+ }
548
+ u
549
+ }
550
+
551
+ /// Token-start bits inside ASCII digit runs for `\p{N}{1,3}`: each run
552
+ /// splits into 3-char tokens, so boundaries sit at run start + 3k. (For a
553
+ /// plain `\p{N}` scheme every digit is a start — no helper needed.)
554
+ #[inline(always)]
555
+ pub(crate) fn digit_run_splits3(d: u64) -> u64 {
556
+ let mut b = d & !(d << 1); // run starts
557
+ // A start at p re-arms at p+3 while the run continues: hop condition
558
+ // c = "p..p+3 all digits". Log-doubling covers 64-bit runs in 5 steps.
559
+ let mut c = d & (d >> 1) & (d >> 2) & (d >> 3);
560
+ let mut sh = 3u32;
561
+ while sh < 64 {
562
+ b |= (b & c) << sh;
563
+ c &= c >> sh;
564
+ sh <<= 1;
565
+ }
566
+ b
567
+ }
568
+
569
+ // -----------------------------------------------------------------------
570
+ // The batch walker
571
+ // -----------------------------------------------------------------------
572
+
573
+ /// The two per-scheme hooks of a mask-scanner pretokenizer.
574
+ pub(crate) trait MaskScheme {
575
+ /// Scalar ground truth: end of the token starting at `pos`
576
+ /// (`pos < bytes.len()`, `pos` on a token boundary).
577
+ fn advance(bytes: &[u8], pos: usize) -> usize;
578
+
579
+ /// `(usable, bad)` for `bytes[scan..scan+64]` (`scan+64 <= len`):
580
+ /// `usable` bit k = trustworthy token start at scan+k; `bad` bit k =
581
+ /// byte scan+k needs the scalar path. `usable & bad` must be 0.
582
+ #[cfg(target_arch = "aarch64")]
583
+ fn batch_masks(bytes: &[u8], scan: usize) -> (u64, u64);
584
+
585
+ /// The x86_64 batch classifier, monomorphized on the SIMD tier
586
+ /// (`AVX512` = true → the AVX-512 front-end, false → AVX2); same
587
+ /// `(usable, bad)` contract as the aarch64 `batch_masks`. The fill
588
+ /// wrappers instantiate this inside a matching `#[target_feature]`
589
+ /// region, so the tier function inlines into the fill loop and no
590
+ /// per-batch dispatch survives (the codegen a `-C target-cpu=native`
591
+ /// build gets).
592
+ ///
593
+ /// # Safety
594
+ ///
595
+ /// The selected tier must have been runtime-detected:
596
+ /// [`avx512_scanner_available`] for `AVX512` = true,
597
+ /// [`avx2_scanner_available`] for `AVX512` = false.
598
+ #[cfg(target_arch = "x86_64")]
599
+ unsafe fn batch_masks_x86<const AVX512: bool>(bytes: &[u8], scan: usize) -> (u64, u64);
600
+
601
+ /// Runtime-dispatched form of [`Self::batch_masks_x86`] for call
602
+ /// sites outside a tier-monomorphized region (`next_span`): a cached
603
+ /// tier check plus a non-inlined call per batch into a per-tier
604
+ /// `#[target_feature]` wrapper ([`batch_masks_dyn_avx512`] /
605
+ /// [`batch_masks_dyn_avx2`]), so the classifier body still compiles
606
+ /// under the full tier feature set. Must only be called when
607
+ /// [`simd_scanner_available`] is true — [`MaskState`] guarantees this
608
+ /// by never leaving the scalar path otherwise.
609
+ #[cfg(target_arch = "x86_64")]
610
+ #[inline(always)]
611
+ fn batch_masks(bytes: &[u8], scan: usize) -> (u64, u64)
612
+ where
613
+ Self: Sized,
614
+ {
615
+ debug_assert!(simd_scanner_available());
616
+ // The tier check is a cached atomic load + bit test and the
617
+ // branch is perfectly predicted, so it is noise next to the
618
+ // batch classification it selects.
619
+ if avx512_scanner_available() {
620
+ // SAFETY: runtime AVX-512 detection right above.
621
+ unsafe { batch_masks_dyn_avx512::<Self>(bytes, scan) }
622
+ } else {
623
+ // SAFETY: MaskState enables the mask-scanner path only after
624
+ // runtime detection (simd_scanner_available); without AVX-512
625
+ // that detection was the AVX2 tier's.
626
+ unsafe { batch_masks_dyn_avx2::<Self>(bytes, scan) }
627
+ }
628
+ }
629
+ }
630
+
631
+ /// AVX-512 feature region for the runtime-dispatched
632
+ /// `MaskScheme::batch_masks`: the scheme's `#[inline(always)]`
633
+ /// `batch_masks_x86` body fuses into this wrapper, so the per-batch call
634
+ /// `next_span` pays runs full-tier codegen (without this region the body
635
+ /// would inline into the plain-feature caller, where the inner
636
+ /// `#[target_feature]` mask classifiers can't inline and the boundary
637
+ /// algebra loses BMI/LZCNT codegen — measured ~25% slower).
638
+ ///
639
+ /// # Safety
640
+ ///
641
+ /// The CPU must support the AVX-512 scanner tier
642
+ /// ([`avx512_scanner_available`]).
643
+ #[cfg(target_arch = "x86_64")]
644
+ #[target_feature(enable = "avx512f,avx512bw,avx512vl,bmi1,bmi2,lzcnt,popcnt")]
645
+ #[inline]
646
+ unsafe fn batch_masks_dyn_avx512<S: MaskScheme>(bytes: &[u8], scan: usize) -> (u64, u64) {
647
+ // SAFETY: the caller detected the AVX-512 tier (fn contract).
648
+ unsafe { S::batch_masks_x86::<true>(bytes, scan) }
649
+ }
650
+
651
+ /// AVX2 counterpart of [`batch_masks_dyn_avx512`].
652
+ ///
653
+ /// # Safety
654
+ ///
655
+ /// The CPU must support the AVX2 scanner tier
656
+ /// ([`avx2_scanner_available`]).
657
+ #[cfg(target_arch = "x86_64")]
658
+ #[target_feature(enable = "avx2,bmi1,bmi2,lzcnt,popcnt")]
659
+ #[inline]
660
+ unsafe fn batch_masks_dyn_avx2<S: MaskScheme>(bytes: &[u8], scan: usize) -> (u64, u64) {
661
+ // SAFETY: the caller detected the AVX2 tier (fn contract).
662
+ unsafe { S::batch_masks_x86::<false>(bytes, scan) }
663
+ }
664
+
665
+ /// x86 SIMD-tier selector for the monomorphized fill bodies
666
+ /// ([`MaskState::fill_spans_two_phase_impl`]): `DYN` keeps the per-batch
667
+ /// runtime dispatch of the provided `MaskScheme::batch_masks`; `AVX2` /
668
+ /// `AVX512` pin the tier, chosen once per fill inside a matching
669
+ /// `#[target_feature]` wrapper. `AVX512_VBMI2` is the AVX-512 tier plus
670
+ /// VBMI2 ([`avx512_fill_available`]): same batch classifiers, but phase
671
+ /// A's flatten runs `vpcompressb` (`flatten_bits_avx512`) — its only
672
+ /// divergence. Meaningless (and always `DYN`) off x86_64.
673
+ pub(crate) const X86_TIER_DYN: u8 = 0;
674
+ pub(crate) const X86_TIER_AVX2: u8 = 1;
675
+ pub(crate) const X86_TIER_AVX512: u8 = 2;
676
+ pub(crate) const X86_TIER_AVX512_VBMI2: u8 = 3;
677
+
678
+ /// Scheme-agnostic mask-scanner state: pops trusted boundary bits, walks
679
+ /// bad zones through the scheme's scalar `advance`, runs the buffer tail
680
+ /// scalar, and precomputes one batch ahead so the SIMD chain retires under
681
+ /// the previous batch's pops. Without SIMD support (non-aarch64/x86_64
682
+ /// targets, or an x86_64 CPU without AVX-512 or AVX2) `scalar_until`
683
+ /// starts at `usize::MAX`, so every token takes the scalar path.
684
+ pub(crate) struct MaskState {
685
+ /// Start of the pending (not yet emitted) token.
686
+ pub pos: usize,
687
+ /// Base of the next batch to scan.
688
+ scan: usize,
689
+ /// Base the `rem`/`batch_*` bits refer to.
690
+ mask_base: usize,
691
+ /// Boundary bits of the current segment (trusted, pop-ready).
692
+ rem: u64,
693
+ /// Full usable mask of the current batch (later segments).
694
+ batch_usable: u64,
695
+ /// Bad zones of the current batch not yet passed.
696
+ batch_bad: u64,
697
+ /// Emit tokens via the scalar advance while `pos < scalar_until`.
698
+ scalar_until: usize,
699
+ /// Eagerly computed masks for the batch at `pre_base` (usize::MAX =
700
+ /// none).
701
+ pre_base: usize,
702
+ pre_usable: u64,
703
+ pre_bad: u64,
704
+ }
705
+
706
+ impl MaskState {
707
+ #[inline]
708
+ pub(crate) fn new(pos: usize) -> Self {
709
+ let scalar_until = if simd_scanner_available() { pos } else { usize::MAX };
710
+ Self {
711
+ pos,
712
+ scan: pos,
713
+ mask_base: pos,
714
+ rem: 0,
715
+ batch_usable: 0,
716
+ batch_bad: 0,
717
+ scalar_until,
718
+ pre_base: usize::MAX,
719
+ pre_usable: 0,
720
+ pre_bad: 0,
721
+ }
722
+ }
723
+
724
+ /// Load the segment of `batch_usable` bits in [from_bit, next bad run)
725
+ /// into `rem` and aim `scalar_until` past that bad run at the next
726
+ /// trusted boundary (or the batch end).
727
+ #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
728
+ #[inline(always)]
729
+ fn load_segment(&mut self, from_bit: u32) {
730
+ let live = u64::MAX << from_bit;
731
+ let seg_bad = self.batch_bad & live;
732
+ if seg_bad == 0 {
733
+ self.rem = self.batch_usable & live;
734
+ self.batch_bad = 0;
735
+ } else {
736
+ let nb = seg_bad.trailing_zeros();
737
+ self.rem = self.batch_usable & live & ((1u64 << nb) - 1);
738
+ let rest = self.batch_usable & (u64::MAX << nb);
739
+ self.scalar_until = if rest != 0 {
740
+ self.mask_base + rest.trailing_zeros() as usize
741
+ } else {
742
+ self.mask_base + 64
743
+ };
744
+ }
745
+ // A bit at the pending token's own start is not an end. Branchless:
746
+ // whether the pending token starts exactly at this segment's first
747
+ // bit is a ~20% coin flip on natural text.
748
+ let at_start = self.pos == self.mask_base + from_bit as usize;
749
+ self.rem &= !(u64::from(at_start) << from_bit);
750
+ }
751
+
752
+ /// The next token's byte range, or None at end of input.
753
+ #[inline(always)]
754
+ pub(crate) fn next_span<S: MaskScheme>(&mut self, bytes: &[u8]) -> Option<(usize, usize)> {
755
+ let len = bytes.len();
756
+ loop {
757
+ if self.rem != 0 {
758
+ let tz = self.rem.trailing_zeros() as usize;
759
+ let end = self.mask_base + tz;
760
+ self.rem &= self.rem - 1;
761
+ let start = self.pos;
762
+ self.pos = end;
763
+ return Some((start, end));
764
+ }
765
+ if self.pos < self.scalar_until {
766
+ if self.pos >= len {
767
+ return None;
768
+ }
769
+ let start = self.pos;
770
+ let end = S::advance(bytes, start);
771
+ self.pos = end;
772
+ return Some((start, end));
773
+ }
774
+ #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
775
+ {
776
+ // Continue with the current batch's next trusted segment
777
+ // after a scalar gap (each batch is computed exactly once).
778
+ if self.batch_bad != 0 && self.pos < self.mask_base + 64 {
779
+ self.load_segment((self.pos - self.mask_base) as u32);
780
+ continue;
781
+ }
782
+ self.batch_bad = 0;
783
+ // Resume after a scalar overrun WITHOUT leaving the
784
+ // 64-byte grid: the precomputed next batch (and the
785
+ // prefetch chain behind it) stays valid, where rebasing
786
+ // to the token boundary invalidated it on every bad-zone
787
+ // overrun — a large part of a deferral's ~800-cycle
788
+ // cost. Grid bits below `pos` may be stale run-internal
789
+ // bits (a ws or digit run the scalar walked through can
790
+ // cross the grid base); they are masked by the
791
+ // `from_bit` passed to load_segment below, and every
792
+ // path that puts `pos` inside such a run goes through a
793
+ // deferral first, so those bits are never trusted.
794
+ while self.scan + 64 <= self.pos {
795
+ self.scan += 64;
796
+ }
797
+ if self.scan + 64 > len {
798
+ // Tail: scalar to the end of the buffer.
799
+ self.scalar_until = usize::MAX;
800
+ continue;
801
+ }
802
+ let (usable, bad) = if self.pre_base == self.scan {
803
+ (self.pre_usable, self.pre_bad)
804
+ } else {
805
+ S::batch_masks(bytes, self.scan)
806
+ };
807
+ self.mask_base = self.scan;
808
+ self.scan += 64;
809
+ self.batch_usable = usable;
810
+ self.batch_bad = bad;
811
+ // Kick off the next batch now; its SIMD chain overlaps this
812
+ // batch's pops instead of stalling the next refill. Also
813
+ // done for dirty batches: a scalar overrun past the batch
814
+ // end just leaves the precompute unused (`pre_base` misses),
815
+ // while gaps that resolve inside the batch — the common
816
+ // case — keep the pipeline primed. Dirty batches used to
817
+ // skip this, and paying the whole SIMD chain latency at the
818
+ // next refill was a large part of their ~270-cycle cost.
819
+ if self.scan + 64 <= len {
820
+ let (u2, b2) = S::batch_masks(bytes, self.scan);
821
+ self.pre_base = self.scan;
822
+ self.pre_usable = u2;
823
+ self.pre_bad = b2;
824
+ } else {
825
+ self.pre_base = usize::MAX;
826
+ }
827
+ // An overrun may have left `pos` inside this grid batch;
828
+ // start from its bit so stale bits below never pop. The
829
+ // no-overrun case keeps the constant argument (and its
830
+ // folded codegen) — schemes with few bad zones take that
831
+ // branch essentially always.
832
+ if self.pos > self.mask_base {
833
+ self.load_segment((self.pos - self.mask_base) as u32);
834
+ } else {
835
+ self.load_segment(0);
836
+ }
837
+ }
838
+ #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
839
+ {
840
+ // Unreachable: scalar_until is usize::MAX on this arch.
841
+ self.scalar_until = usize::MAX;
842
+ }
843
+ }
844
+ }
845
+ }
846
+
847
+ // -----------------------------------------------------------------------
848
+ // Two-phase chunked span fill
849
+ // -----------------------------------------------------------------------
850
+
851
+ /// Set-bit positions of a byte, packed in 8 u16 lanes (unused lanes 0,
852
+ /// never read). 4 KB, L1-resident alongside the unicode class table.
853
+ #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
854
+ static BIT_POS: [[u16; 8]; 256] = {
855
+ let mut t = [[0u16; 8]; 256];
856
+ let mut b = 1usize;
857
+ while b < 256 {
858
+ let mut j = 0;
859
+ let mut w = 0;
860
+ while j < 8 {
861
+ if b >> j & 1 == 1 {
862
+ t[b][w] = j as u16;
863
+ w += 1;
864
+ }
865
+ j += 1;
866
+ }
867
+ b += 1;
868
+ }
869
+ t
870
+ };
871
+
872
+ /// Append the set-bit positions of `m`, offset by `rel` (wrapping), to
873
+ /// `out[0..popcount]` with no data-dependent branch: 8 fixed iterations,
874
+ /// one unconditional 8-lane store each at that octet's exclusive-prefix
875
+ /// popcount, so 64 bits cost the same straight-line code regardless of
876
+ /// population. Scribbles up to `out[popcount + 7]`; callers reserve the
877
+ /// slack. Returns the popcount.
878
+ #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
879
+ #[inline(always)]
880
+ unsafe fn flatten_bits(m: u64, rel: u16, out: *mut u16) -> usize {
881
+ // Per-octet popcounts (SWAR); one multiply turns them into inclusive
882
+ // prefix sums, and a byte shift makes them exclusive write offsets —
883
+ // the 8 stores below are mutually independent.
884
+ let mut x = m;
885
+ x -= (x >> 1) & 0x5555_5555_5555_5555;
886
+ x = (x & 0x3333_3333_3333_3333) + ((x >> 2) & 0x3333_3333_3333_3333);
887
+ x = (x + (x >> 4)) & 0x0F0F_0F0F_0F0F_0F0F;
888
+ let incl = x.wrapping_mul(0x0101_0101_0101_0101);
889
+ let excl = incl << 8;
890
+ #[cfg(target_arch = "aarch64")]
891
+ unsafe {
892
+ use std::arch::aarch64::*;
893
+ for j in 0..8 {
894
+ let b = (m >> (8 * j)) as u8 as usize;
895
+ let w = (excl >> (8 * j)) as u8 as usize;
896
+ let v = vld1q_u16(BIT_POS[b].as_ptr());
897
+ let v = vaddq_u16(v, vdupq_n_u16(rel.wrapping_add(8 * j as u16)));
898
+ vst1q_u16(out.add(w), v);
899
+ }
900
+ }
901
+ #[cfg(not(target_arch = "aarch64"))]
902
+ unsafe {
903
+ for j in 0..8 {
904
+ let b = (m >> (8 * j)) as u8 as usize;
905
+ let w = (excl >> (8 * j)) as u8 as usize;
906
+ let e = &BIT_POS[b];
907
+ let base = rel.wrapping_add(8 * j as u16);
908
+ // Fixed 8-lane copy: autovectorizes to one 16-byte store.
909
+ for t in 0..8 {
910
+ out.add(w + t).write(e[t].wrapping_add(base));
911
+ }
912
+ }
913
+ }
914
+ (incl >> 56) as usize
915
+ }
916
+
917
+ /// [`flatten_bits`] via AVX-512 VBMI2: `vpcompressb` packs the set-bit
918
+ /// positions of `m` (as compressed iota-byte lanes) in one op, replacing
919
+ /// the 8-octet BIT_POS LUT walk (~3.8% of warm cycles on Zen 5,
920
+ /// profiling/zen5_st_profile.md §5.5). Widen both halves to u16, add the
921
+ /// broadcast `rel` (wrapping, as the scalar version), two unconditional
922
+ /// 64-byte stores. Scribbles `out[0..128]` regardless of popcount — a
923
+ /// wider scribble than the scalar version's `out[popcount + 7]`; BOUND_BUF
924
+ /// reserves the 128-lane slack past every call site's worst-case cursor.
925
+ ///
926
+ /// # Safety
927
+ ///
928
+ /// The CPU must support AVX-512 F/BW/VBMI2 (reached only from the
929
+ /// `fill_spans_two_phase_avx512_vbmi2_crc` wrapper, gated on
930
+ /// [`avx512_fill_available`]), and `out[0..128]` must be writable.
931
+ #[cfg(target_arch = "x86_64")]
932
+ #[target_feature(enable = "avx512f,avx512bw,avx512vbmi2")]
933
+ #[inline]
934
+ unsafe fn flatten_bits_avx512(m: u64, rel: u16, out: *mut u16) -> usize {
935
+ use std::arch::x86_64::*;
936
+ const IOTA: [u8; 64] = {
937
+ let mut a = [0u8; 64];
938
+ let mut i = 0;
939
+ while i < 64 {
940
+ a[i] = i as u8;
941
+ i += 1;
942
+ }
943
+ a
944
+ };
945
+ unsafe {
946
+ let iota = _mm512_loadu_si512(IOTA.as_ptr() as *const _);
947
+ let comp = _mm512_maskz_compress_epi8(m, iota);
948
+ let relv = _mm512_set1_epi16(rel as i16);
949
+ let lo = _mm512_add_epi16(_mm512_cvtepu8_epi16(_mm512_castsi512_si256(comp)), relv);
950
+ let hi = _mm512_add_epi16(
951
+ _mm512_cvtepu8_epi16(_mm512_extracti64x4_epi64::<1>(comp)),
952
+ relv,
953
+ );
954
+ _mm512_storeu_si512(out as *mut _, lo);
955
+ _mm512_storeu_si512(out.add(32) as *mut _, hi);
956
+ }
957
+ m.count_ones() as usize
958
+ }
959
+
960
+ /// [`flatten_bits`], monomorphized on the fill's x86 tier: the const
961
+ /// comparison folds at compile time (no per-call branch survives), and
962
+ /// the VBMI2 variant is only instantiated live inside the
963
+ /// `#[target_feature]` `_avx512_vbmi2_crc` wrapper, so its intrinsics
964
+ /// inline there.
965
+ ///
966
+ /// # Safety
967
+ ///
968
+ /// As [`flatten_bits`]; with `X86_TIER = X86_TIER_AVX512_VBMI2`,
969
+ /// additionally [`flatten_bits_avx512`]'s contract (VBMI2 CPU, 128
970
+ /// writable lanes).
971
+ #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
972
+ #[inline(always)]
973
+ unsafe fn flatten_bits_dispatch<const X86_TIER: u8>(m: u64, rel: u16, out: *mut u16) -> usize {
974
+ #[cfg(target_arch = "x86_64")]
975
+ if X86_TIER == X86_TIER_AVX512_VBMI2 {
976
+ // SAFETY: forwarded from this fn's contract.
977
+ return unsafe { flatten_bits_avx512(m, rel, out) };
978
+ }
979
+ // SAFETY: forwarded from this fn's contract.
980
+ unsafe { flatten_bits(m, rel, out) }
981
+ }
982
+
983
+ /// [`pack_mask_halves`](crate::pretokenize::pack_mask_halves) — the single
984
+ /// source of the mask math — evaluated for each clamped length `m` in
985
+ /// 1..=15 (entry 0 unused), as one 16-byte row so the phase-B emission
986
+ /// loop loads both halves with a single `ldp`. That loop is
987
+ /// issue-width-bound (~34 instructions/span before, at 4 stores + 2 loads
988
+ /// it is nowhere near the load/store port limits), so trading the 7-op
989
+ /// per-half shift/select chain for 1 always-L1-hot load (256 B, 4 lines)
990
+ /// is a straight instruction-count cut. The ALU form stays in
991
+ /// `pack_mask_halves` for the latency-chained per-span paths — see its
992
+ /// docs for the measured cost of a dependent load on those chains.
993
+ #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
994
+ static PACK_MASK_TABLE: [[u64; 2]; 16] = {
995
+ let mut t = [[0u64; 2]; 16];
996
+ let mut n = 1;
997
+ while n <= 15 {
998
+ let (lo, hi) = crate::pretokenize::pack_mask_halves(n);
999
+ t[n] = [lo, hi];
1000
+ n += 1;
1001
+ }
1002
+ t
1003
+ };
1004
+
1005
+ /// Boundary scratch of one fill: PRETOKEN_CHUNK live entries, one batch of
1006
+ /// overshoot from the last harvested batch (64 in-batch boundaries plus one
1007
+ /// scalar-overrun end), and the flatten scribble slack — 128 lanes for
1008
+ /// [`flatten_bits_avx512`]'s two unconditional 64-byte stores (the widest
1009
+ /// path; [`flatten_bits`]' 8-lane scribble is subsumed), with margin.
1010
+ /// Worst-case cursor at a flatten call: needed - 1 (= 255) at batch entry
1011
+ /// plus up to 64 in-batch boundaries already written = 319; 319 + 128 =
1012
+ /// 447 <= 464.
1013
+ #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
1014
+ const BOUND_BUF: usize = crate::pretokenize::PRETOKEN_CHUNK + 208;
1015
+
1016
+ /// Boundary offsets are u16-relative to the fill base; a batch is only
1017
+ /// harvested while every position it can contribute (base + 63, or the
1018
+ /// tail's `len`, both < base + 64) still fits.
1019
+ #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
1020
+ const REL_LIMIT: isize = u16::MAX as isize - 127;
1021
+
1022
+ #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
1023
+ impl MaskState {
1024
+ /// Two-phase `fill_spans_keyed` body: phase A harvests one chunk's
1025
+ /// boundary positions into a flat buffer (branchless [`flatten_bits`]
1026
+ /// per clean batch; the scheme's scalar `advance` through bad zones,
1027
+ /// with `next_span`'s exact segment/overrun/tail trust rules), then
1028
+ /// phase B turns consecutive boundary pairs into batch entries
1029
+ /// in a counted loop with no data-dependent branch. The per-span
1030
+ /// refill ladder, pop-exit and pack mispredicts of the fused
1031
+ /// `next_span` loop (the dominant share of encode's 25% discarded
1032
+ /// issue bandwidth) collapse into one predictable branch per 64-byte
1033
+ /// batch.
1034
+ ///
1035
+ /// Boundary sets are identical to `next_span`'s by construction: the
1036
+ /// same `batch_masks` bits, the same scalar re-derivation for bad
1037
+ /// zones. Leftover boundaries past the chunk are discarded and `scan`
1038
+ /// rewound to the grid batch containing `pos` — masks are pure
1039
+ /// functions of the bytes, so the ~1 recomputed batch per fill buys
1040
+ /// carry-free fills, and a later `next_span` (which only ever advances
1041
+ /// `scan`) cannot skip the discarded bits. All other fields are reset
1042
+ /// so iterator and chunked pulls compose in any order.
1043
+ ///
1044
+ /// Callers must ensure [`simd_scanner_available`] (the scheme's
1045
+ /// `batch_masks` is unsafe to call otherwise on x86_64).
1046
+ #[inline(always)]
1047
+ pub(crate) fn fill_spans_two_phase<'a, S: MaskScheme>(
1048
+ &mut self,
1049
+ bytes: &'a [u8],
1050
+ batch: &mut crate::pretokenize::SpanBatch<'a>,
1051
+ prefetch: &impl Fn(u64),
1052
+ ) -> usize {
1053
+ // Tier + hash-arm dispatch, once per fill (≤ PRETOKEN_CHUNK
1054
+ // spans), on process-immutable bits (see `fill_span_hash` for the
1055
+ // hash-arm contract). Inside the tier wrappers the scheme's batch
1056
+ // classifier inlines into the harvest loop and the bit-scan loops
1057
+ // get BMI/LZCNT codegen — the per-batch tier branch and call that
1058
+ // the provided `MaskScheme::batch_masks` pays (~2% of end-to-end
1059
+ // encode) exist only in the DYN instantiation, which real
1060
+ // hardware never takes: fill callers require a SIMD tier, and
1061
+ // every AVX2/AVX-512 CPU has SSE4.2.
1062
+ #[cfg(target_arch = "x86_64")]
1063
+ if crate::pretokenize::crc_hash_selected() {
1064
+ // Feature detection (including the VBMI2 bit) stays in this
1065
+ // once-per-fill dispatch: `is_x86_feature_detected!` does NOT
1066
+ // const-fold inside a matching `#[target_feature]` fn (it
1067
+ // stays an atomic load), so testing it any deeper would put
1068
+ // the load in the loop.
1069
+ if avx512_fill_available() {
1070
+ // SAFETY: `avx512_fill_available` verified the AVX-512
1071
+ // scanner tier plus VBMI2; every such CPU has SSE4.2
1072
+ // (also implied by `crc_hash_selected` above).
1073
+ return unsafe {
1074
+ self.fill_spans_two_phase_avx512_vbmi2_crc::<S>(bytes, batch, prefetch)
1075
+ };
1076
+ }
1077
+ if avx512_scanner_available() {
1078
+ // SAFETY: AVX-512 tier + SSE4.2 detected right above.
1079
+ return unsafe {
1080
+ self.fill_spans_two_phase_avx512_crc::<S>(bytes, batch, prefetch)
1081
+ };
1082
+ }
1083
+ if avx2_scanner_available() {
1084
+ // SAFETY: AVX2 tier + SSE4.2 detected right above.
1085
+ return unsafe {
1086
+ self.fill_spans_two_phase_avx2_crc::<S>(bytes, batch, prefetch)
1087
+ };
1088
+ }
1089
+ // SAFETY: `crc_hash_selected` verified SSE4.2 support.
1090
+ return unsafe { self.fill_spans_two_phase_crc::<S>(bytes, batch, prefetch) };
1091
+ }
1092
+ self.fill_spans_two_phase_impl::<S, false, X86_TIER_DYN>(bytes, batch, prefetch)
1093
+ }
1094
+
1095
+ /// The AVX-512 + VBMI2 tier, CRC-hash monomorphization of
1096
+ /// [`Self::fill_spans_two_phase`] (Zen 4/5, Ice Lake+): the AVX-512
1097
+ /// tier wrapper plus `avx512vbmi2` for `flatten_bits_avx512` — its
1098
+ /// only divergence from [`Self::fill_spans_two_phase_avx512_crc`],
1099
+ /// which stays the tier for AVX-512 CPUs without VBMI2 (Skylake-X).
1100
+ /// An AVX-512 phase-B key pack measured a large regression, see the
1101
+ /// phase-B loop's comment.
1102
+ ///
1103
+ /// # Safety
1104
+ ///
1105
+ /// The CPU must support the AVX-512 scanner tier plus VBMI2
1106
+ /// ([`avx512_fill_available`]) and SSE4.2 (`crc_hash_selected`).
1107
+ #[cfg(target_arch = "x86_64")]
1108
+ #[target_feature(
1109
+ enable = "avx512f,avx512bw,avx512vl,avx512vbmi2,bmi1,bmi2,lzcnt,popcnt,sse4.2"
1110
+ )]
1111
+ unsafe fn fill_spans_two_phase_avx512_vbmi2_crc<'a, S: MaskScheme>(
1112
+ &mut self,
1113
+ bytes: &'a [u8],
1114
+ batch: &mut crate::pretokenize::SpanBatch<'a>,
1115
+ prefetch: &impl Fn(u64),
1116
+ ) -> usize {
1117
+ self.fill_spans_two_phase_impl::<S, true, X86_TIER_AVX512_VBMI2>(bytes, batch, prefetch)
1118
+ }
1119
+
1120
+ /// The AVX-512-tier, CRC-hash monomorphization of
1121
+ /// [`Self::fill_spans_two_phase`]. The feature set is the AVX-512
1122
+ /// scanner tier plus `sse4.2` for the CRC hash arm (implied by
1123
+ /// `avx512f`, spelled out because the `X86_CRC = true` body requires
1124
+ /// it — see `fill_span_hash`'s reachability contract).
1125
+ ///
1126
+ /// # Safety
1127
+ ///
1128
+ /// The CPU must support the AVX-512 scanner tier
1129
+ /// ([`avx512_scanner_available`]) and SSE4.2 (`crc_hash_selected`).
1130
+ #[cfg(target_arch = "x86_64")]
1131
+ #[target_feature(enable = "avx512f,avx512bw,avx512vl,bmi1,bmi2,lzcnt,popcnt,sse4.2")]
1132
+ unsafe fn fill_spans_two_phase_avx512_crc<'a, S: MaskScheme>(
1133
+ &mut self,
1134
+ bytes: &'a [u8],
1135
+ batch: &mut crate::pretokenize::SpanBatch<'a>,
1136
+ prefetch: &impl Fn(u64),
1137
+ ) -> usize {
1138
+ self.fill_spans_two_phase_impl::<S, true, X86_TIER_AVX512>(bytes, batch, prefetch)
1139
+ }
1140
+
1141
+ /// The AVX2-tier, CRC-hash monomorphization of
1142
+ /// [`Self::fill_spans_two_phase`] (Haswell+, Zen 1-3; `sse4.2` is
1143
+ /// implied by `avx` but spelled out for the CRC arm's contract).
1144
+ ///
1145
+ /// # Safety
1146
+ ///
1147
+ /// The CPU must support the AVX2 scanner tier
1148
+ /// ([`avx2_scanner_available`]) and SSE4.2 (`crc_hash_selected`).
1149
+ #[cfg(target_arch = "x86_64")]
1150
+ #[target_feature(enable = "avx2,bmi1,bmi2,lzcnt,popcnt,sse4.2")]
1151
+ unsafe fn fill_spans_two_phase_avx2_crc<'a, S: MaskScheme>(
1152
+ &mut self,
1153
+ bytes: &'a [u8],
1154
+ batch: &mut crate::pretokenize::SpanBatch<'a>,
1155
+ prefetch: &impl Fn(u64),
1156
+ ) -> usize {
1157
+ self.fill_spans_two_phase_impl::<S, true, X86_TIER_AVX2>(bytes, batch, prefetch)
1158
+ }
1159
+
1160
+ /// The SSE4.2-only (CRC-hash, per-batch tier dispatch)
1161
+ /// monomorphization of [`Self::fill_spans_two_phase`]: unreachable on
1162
+ /// real hardware (every AVX2/AVX-512 CPU has SSE4.2, so one of the
1163
+ /// tier wrappers wins), kept for CPUID-masking hypervisors.
1164
+ ///
1165
+ /// # Safety
1166
+ ///
1167
+ /// The CPU must support SSE4.2 (`crc_hash_selected` must have
1168
+ /// returned true). The caller must also uphold
1169
+ /// [`Self::fill_spans_two_phase`]'s own precondition
1170
+ /// ([`simd_scanner_available`]).
1171
+ #[cfg(target_arch = "x86_64")]
1172
+ #[target_feature(enable = "sse4.2")]
1173
+ unsafe fn fill_spans_two_phase_crc<'a, S: MaskScheme>(
1174
+ &mut self,
1175
+ bytes: &'a [u8],
1176
+ batch: &mut crate::pretokenize::SpanBatch<'a>,
1177
+ prefetch: &impl Fn(u64),
1178
+ ) -> usize {
1179
+ self.fill_spans_two_phase_impl::<S, true, X86_TIER_DYN>(bytes, batch, prefetch)
1180
+ }
1181
+
1182
+ /// [`Self::fill_spans_two_phase`]'s body, monomorphized on the hash
1183
+ /// arm (`X86_CRC` — see `fill_span_hash`'s reachability contract) and
1184
+ /// the x86 SIMD tier (`X86_TIER` — the `X86_TIER_*` constants; the
1185
+ /// AVX2/AVX-512/VBMI2 instantiations are only reachable through the
1186
+ /// matching `#[target_feature]` wrappers above, whose feature sets
1187
+ /// cover every intrinsic their tier's arms use).
1188
+ #[inline(always)]
1189
+ fn fill_spans_two_phase_impl<'a, S: MaskScheme, const X86_CRC: bool, const X86_TIER: u8>(
1190
+ &mut self,
1191
+ bytes: &'a [u8],
1192
+ batch: &mut crate::pretokenize::SpanBatch<'a>,
1193
+ prefetch: &impl Fn(u64),
1194
+ ) -> usize {
1195
+ use crate::pretokenize::{
1196
+ PRETOKEN_CHUNK, fill_span_hash, pack_pretoken_key,
1197
+ };
1198
+ debug_assert!(simd_scanner_available());
1199
+ let len = bytes.len();
1200
+ let mut pending = self.pos;
1201
+ let mut scan = self.scan;
1202
+ // Rewind onto the grid batch containing `pending` after iterator
1203
+ // pops (next_span keeps consumed batches' bits in `rem`, which
1204
+ // this path recomputes). The forward direction is normalized at
1205
+ // each refill below.
1206
+ if scan > pending {
1207
+ scan -= 64 * (scan - pending).div_ceil(64);
1208
+ }
1209
+ let mut n = 0usize;
1210
+ // Opaque table base: LLVM rematerializes the static's address as
1211
+ // an adrp+add pair inside the per-span emission loop (constant
1212
+ // addresses are "free to recompute" to the register allocator);
1213
+ // pinning it here keeps the loop at one indexed ldp per span.
1214
+ let pack_masks: *const [u64; 2] = std::hint::black_box(PACK_MASK_TABLE.as_ptr());
1215
+
1216
+ 'refill: while n < PRETOKEN_CHUNK && pending < len {
1217
+ // Skip grid batches wholly behind `pending` (a direct-emitted
1218
+ // long span or a dropped overrun end can leave `scan` far
1219
+ // back); keeps `resume - base <= 63` for every batch below.
1220
+ if pending >= scan + 64 {
1221
+ scan += 64 * ((pending - scan) / 64);
1222
+ }
1223
+ let fill_base = pending;
1224
+ let needed = PRETOKEN_CHUNK - n;
1225
+ let mut buf = [std::mem::MaybeUninit::<u16>::uninit(); BOUND_BUF];
1226
+ let bufp = buf.as_mut_ptr() as *mut u16;
1227
+ let mut nb = 0usize;
1228
+ // Boundary bits at or below `resume` are settled: the pending
1229
+ // token's own start, or stale run-internal bits behind a
1230
+ // scalar overrun (see next_span's grid-keeping comment).
1231
+ let mut resume = pending;
1232
+ let mut exhausted = false;
1233
+ // A scalar end past the u16 window: dropped and re-derived
1234
+ // next fill, unless it is the fill's first boundary (emitted
1235
+ // directly below).
1236
+ let mut overflow_end: Option<usize> = None;
1237
+
1238
+ // Phase A: harvest boundary positions.
1239
+ 'harvest: while nb < needed {
1240
+ if scan.wrapping_sub(fill_base) as isize > REL_LIMIT {
1241
+ break; // re-base: offsets would leave the u16 window
1242
+ }
1243
+ if scan + 64 > len {
1244
+ // Scalar tail to end of input.
1245
+ let mut p = if nb > 0 {
1246
+ fill_base + unsafe { *bufp.add(nb - 1) } as usize
1247
+ } else {
1248
+ fill_base
1249
+ };
1250
+ while p < len && nb < needed {
1251
+ p = S::advance(bytes, p);
1252
+ // p <= len < scan + 64, within the u16 window per
1253
+ // the REL_LIMIT check above.
1254
+ unsafe { bufp.add(nb).write((p - fill_base) as u16) };
1255
+ nb += 1;
1256
+ }
1257
+ exhausted = p >= len;
1258
+ break;
1259
+ }
1260
+ let base = scan;
1261
+ #[cfg(target_arch = "x86_64")]
1262
+ let (usable, bad) = match X86_TIER {
1263
+ // SAFETY: the tier wrappers instantiate these arms
1264
+ // only after runtime tier detection (see
1265
+ // `fill_spans_two_phase`).
1266
+ // The VBMI2 tier runs the same AVX-512 classifiers;
1267
+ // it only diverges in the flatten below.
1268
+ X86_TIER_AVX512 | X86_TIER_AVX512_VBMI2 => unsafe {
1269
+ S::batch_masks_x86::<true>(bytes, base)
1270
+ },
1271
+ X86_TIER_AVX2 => unsafe { S::batch_masks_x86::<false>(bytes, base) },
1272
+ _ => S::batch_masks(bytes, base),
1273
+ };
1274
+ #[cfg(not(target_arch = "x86_64"))]
1275
+ let (usable, bad) = S::batch_masks(bytes, base);
1276
+ // At a resume point r (the pending token's start): usable
1277
+ // bits at or below r are dead (the pending start itself,
1278
+ // or stale run-internal bits behind a scalar overrun), but
1279
+ // a bad bit AT r must stay live — load_segment's
1280
+ // `live = MAX << from_bit` plus the at_start clear. A zone
1281
+ // starting exactly at r has to route r through the scalar
1282
+ // path, or the stale post-zone usable bit would be
1283
+ // trusted. Only the fill's first batch and post-overrun
1284
+ // batches have such bits.
1285
+ let (mut ulive, mut blive) = if resume >= base {
1286
+ debug_assert!(resume - base < 64);
1287
+ let k = resume - base;
1288
+ ((u64::MAX << k) << 1, u64::MAX << k)
1289
+ } else {
1290
+ (u64::MAX, u64::MAX)
1291
+ };
1292
+ let rel = base.wrapping_sub(fill_base) as u16;
1293
+ if bad & blive == 0 {
1294
+ // Scribble bound: 128 lanes (VBMI2 tier) / popcount +
1295
+ // 7 (scalar) — see BOUND_BUF's worst-case analysis.
1296
+ debug_assert!(nb + if X86_TIER == X86_TIER_AVX512_VBMI2 { 128 } else { 72 } <= BOUND_BUF);
1297
+ nb += unsafe {
1298
+ flatten_bits_dispatch::<X86_TIER>(usable & ulive, rel, bufp.add(nb))
1299
+ };
1300
+ scan = base + 64;
1301
+ continue;
1302
+ }
1303
+ // Dirty batch: per segment, trusted prefix bits then the
1304
+ // scheme's scalar advance through the zone up to the next
1305
+ // trusted boundary — load_segment's rules, emitting into
1306
+ // the buffer.
1307
+ loop {
1308
+ let seg_bad = bad & blive;
1309
+ if seg_bad == 0 {
1310
+ debug_assert!(nb + if X86_TIER == X86_TIER_AVX512_VBMI2 { 128 } else { 72 } <= BOUND_BUF);
1311
+ nb += unsafe {
1312
+ flatten_bits_dispatch::<X86_TIER>(usable & ulive, rel, bufp.add(nb))
1313
+ };
1314
+ scan = base + 64;
1315
+ break;
1316
+ }
1317
+ let fb = seg_bad.trailing_zeros();
1318
+ let prefix = usable & ulive & !(u64::MAX << fb);
1319
+ debug_assert!(nb + if X86_TIER == X86_TIER_AVX512_VBMI2 { 128 } else { 72 } <= BOUND_BUF);
1320
+ nb += unsafe { flatten_bits_dispatch::<X86_TIER>(prefix, rel, bufp.add(nb)) };
1321
+ let mut p = if nb > 0 {
1322
+ fill_base + unsafe { *bufp.add(nb - 1) } as usize
1323
+ } else {
1324
+ fill_base
1325
+ };
1326
+ let rest = usable & (u64::MAX << fb);
1327
+ let until = if rest != 0 {
1328
+ base + rest.trailing_zeros() as usize
1329
+ } else {
1330
+ base + 64
1331
+ };
1332
+ // until <= base + 64 <= len, so `advance` stays in
1333
+ // bounds; it may overrun `until` and the batch end.
1334
+ while p < until {
1335
+ p = S::advance(bytes, p);
1336
+ let relp = p - fill_base;
1337
+ if relp > u16::MAX as usize {
1338
+ overflow_end = Some(p);
1339
+ break 'harvest;
1340
+ }
1341
+ debug_assert!(nb < BOUND_BUF);
1342
+ unsafe { bufp.add(nb).write(relp as u16) };
1343
+ nb += 1;
1344
+ }
1345
+ if p >= base + 64 {
1346
+ // Overrun past the batch: stay on the grid and
1347
+ // resume in the batch containing p, bits at or
1348
+ // below p masked (they can be stale run-internal
1349
+ // bits, exactly as in next_span).
1350
+ scan = base + 64 * ((p - base) / 64);
1351
+ resume = p;
1352
+ break;
1353
+ }
1354
+ // Resume inside the batch at p: same at-start/bad-bit
1355
+ // split as the batch-entry masks above.
1356
+ blive = u64::MAX << (p - base);
1357
+ ulive = blive << 1;
1358
+ }
1359
+ }
1360
+
1361
+ if nb == 0 {
1362
+ // No boundary inside the u16 window: a > 65 KB pretoken.
1363
+ // Emit it alone through the careful pack.
1364
+ debug_assert!(!exhausted);
1365
+ let end = overflow_end.unwrap_or_else(|| S::advance(bytes, fill_base));
1366
+ let span = &bytes[fill_base..end];
1367
+ let (key, h) = match pack_pretoken_key(span) {
1368
+ Some(key) => (key, fill_span_hash::<X86_CRC>(key)),
1369
+ None => (0, 0),
1370
+ };
1371
+ prefetch(h);
1372
+ let meta = if key != 0 { h } else { span.len() as u64 };
1373
+ batch.entries[n] = crate::pretokenize::BatchEntry {
1374
+ key,
1375
+ ptr: span.as_ptr(),
1376
+ meta,
1377
+ };
1378
+ n += 1;
1379
+ pending = end;
1380
+ continue 'refill;
1381
+ }
1382
+
1383
+ // Phase B: flat emission with no data-dependent branch. One
1384
+ // hoisted check proves every 16-byte key load in-bounds of the
1385
+ // input slice; only a fill reaching within 16 bytes of EOF
1386
+ // routes through the careful per-span pack.
1387
+ let emit_n = nb.min(needed);
1388
+ let last_end = unsafe { *bufp.add(emit_n - 1) } as usize;
1389
+ let entries = &mut batch.entries[n..n + emit_n];
1390
+ let base_ptr = unsafe { bytes.as_ptr().add(fill_base) };
1391
+ // `prev`/`end` in usize: the u16 boundary domain forced two
1392
+ // `& 0xffff` masks and a duplicated 15-compare per span (the
1393
+ // compiler cannot see end >= prev in u16 subtraction).
1394
+ let mut prev = 0usize;
1395
+ if fill_base + last_end + 16 <= len {
1396
+ // Every x86 tier shares this scalar key pack. An AVX-512
1397
+ // masked pack (`vmovdqu8 {k}{z}` under a tok_len-derived
1398
+ // kmask, vpextrq/vmovq into the CRC) measured −36% warm /
1399
+ // −30% cold on Zen 5 (5-round interleaved A/B, 1 GB
1400
+ // gpt2, tokens identical): this plain load's address
1401
+ // depends only on `prev`, so it issues early and the
1402
+ // table row + ANDs apply late, while the masked load's
1403
+ // kmask waits on the whole boundary→tok_len chain and the
1404
+ // extracts add a vector→GPR crossing before the CRC —
1405
+ // per-span work went from overlapping to serialized
1406
+ // (~90% of loop samples on the masked load + dependents).
1407
+ // Do not re-try; the VBMI2 tier's only divergence is
1408
+ // `flatten_bits_avx512` in phase A.
1409
+ for (i, e) in entries.iter_mut().enumerate() {
1410
+ let end = unsafe { *bufp.add(i) } as usize;
1411
+ let tok_len = end - prev;
1412
+ let p = unsafe { base_ptr.add(prev) };
1413
+ prev = end;
1414
+ // SAFETY: p + 16 <= base_ptr + last_end + 16 <= end of
1415
+ // the input slice (hoisted check above).
1416
+ let raw = unsafe { (p as *const u128).read_unaligned() };
1417
+ // Branchless pack_pretoken_key: one ldp from
1418
+ // PACK_MASK_TABLE instead of the 7-op per-half ALU
1419
+ // chain (see the table's docs). tok_len >= 1
1420
+ // (boundaries are strictly increasing), so the clamped
1421
+ // length is in the table's 1..=15 domain. Long spans
1422
+ // take key 0 (pretoken_key_hash(0) == 0) through the
1423
+ // `keep` AND-mask — an if/select here gets if-converted
1424
+ // into a real branch (LLVM hoists it to skip the two
1425
+ // loads), reintroducing the pattern-free n > 15 branch
1426
+ // this loop exists to avoid.
1427
+ let m = tok_len.min(15);
1428
+ // SAFETY: m <= 15, in the 16-entry table.
1429
+ let [mask_lo, mask_hi] = unsafe { *pack_masks.add(m) };
1430
+ let keep = ((tok_len <= 15) as u64).wrapping_neg();
1431
+ let klo = (raw as u64) & mask_lo & keep;
1432
+ let khi = (((raw >> 64) as u64 & mask_hi) | ((m as u64) << 56)) & keep;
1433
+ let key = (klo as u128) | ((khi as u128) << 64);
1434
+ let hv = fill_span_hash::<X86_CRC>(key);
1435
+ prefetch(hv);
1436
+ // meta = hash for short spans, length for long ones
1437
+ // (see `BatchEntry::meta`), in the same AND-mask style
1438
+ // as the key routing — a select gets if-converted.
1439
+ let meta = (hv & keep) | (tok_len as u64 & !keep);
1440
+ e.key = key;
1441
+ e.ptr = p;
1442
+ e.meta = meta;
1443
+ }
1444
+ } else {
1445
+ for (i, e) in entries.iter_mut().enumerate() {
1446
+ let end = unsafe { *bufp.add(i) } as usize;
1447
+ let tok_len = end - prev;
1448
+ let p = unsafe { base_ptr.add(prev) };
1449
+ prev = end;
1450
+ // SAFETY: as above for the span bounds.
1451
+ let span = unsafe { std::slice::from_raw_parts(p, tok_len) };
1452
+ let (key, hv) = match pack_pretoken_key(span) {
1453
+ Some(key) => (key, fill_span_hash::<X86_CRC>(key)),
1454
+ None => (0, 0),
1455
+ };
1456
+ prefetch(hv);
1457
+ let meta = if key != 0 { hv } else { tok_len as u64 };
1458
+ e.key = key;
1459
+ e.ptr = p;
1460
+ e.meta = meta;
1461
+ }
1462
+ }
1463
+ n += emit_n;
1464
+ pending = fill_base + prev;
1465
+ if exhausted {
1466
+ debug_assert_eq!(pending, len);
1467
+ break;
1468
+ }
1469
+ }
1470
+
1471
+ // Rewind past discarded leftover boundaries and leave the state as
1472
+ // a fresh resume at `pending` for either pull style.
1473
+ if scan > pending {
1474
+ scan -= 64 * (scan - pending).div_ceil(64);
1475
+ }
1476
+ self.pos = pending;
1477
+ self.scan = scan;
1478
+ self.mask_base = scan;
1479
+ self.rem = 0;
1480
+ self.batch_usable = 0;
1481
+ self.batch_bad = 0;
1482
+ self.scalar_until = pending;
1483
+ self.pre_base = usize::MAX;
1484
+ n
1485
+ }
1486
+ }