gigatoken 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Cargo.lock +3016 -0
- data/Cargo.toml +135 -0
- data/LICENSE +21 -0
- data/README.md +141 -0
- data/exe/gigatoken +9 -0
- data/ext/gigatoken/Cargo.toml +24 -0
- data/ext/gigatoken/extconf.rb +19 -0
- data/ext/gigatoken/src/error.rs +20 -0
- data/ext/gigatoken/src/gvl.rs +122 -0
- data/ext/gigatoken/src/lib.rs +50 -0
- data/ext/gigatoken/src/sentencepiece.rs +205 -0
- data/ext/gigatoken/src/sources.rs +207 -0
- data/ext/gigatoken/src/tokenizer.rs +571 -0
- data/lib/gigatoken/cli/bench.rb +64 -0
- data/lib/gigatoken/cli/support.rb +132 -0
- data/lib/gigatoken/cli/validate.rb +55 -0
- data/lib/gigatoken/cli.rb +18 -0
- data/lib/gigatoken/hub.rb +242 -0
- data/lib/gigatoken/packed_result.rb +48 -0
- data/lib/gigatoken/tokenizer.rb +117 -0
- data/lib/gigatoken/version.rb +5 -0
- data/lib/gigatoken.rb +29 -0
- data/rust-toolchain.toml +8 -0
- data/src/batch.rs +1808 -0
- data/src/bindings/bridge.rs +396 -0
- data/src/bindings/hub.rs +42 -0
- data/src/bindings/matcher.rs +114 -0
- data/src/bindings/mod.rs +14 -0
- data/src/bindings/padding.rs +177 -0
- data/src/bindings/pretokenize.rs +53 -0
- data/src/bindings/sources.rs +273 -0
- data/src/bindings/train.rs +125 -0
- data/src/bpe/mod.rs +1217 -0
- data/src/bpe/pretoken_cache.rs +495 -0
- data/src/bpe/sentencepiece.rs +1485 -0
- data/src/bpe/tiktoken.rs +2555 -0
- data/src/bpe_train.rs +351 -0
- data/src/input/decompress.rs +11 -0
- data/src/input/file_source.rs +514 -0
- data/src/input/jsonl.rs +94 -0
- data/src/input/mod.rs +333 -0
- data/src/input/parquet.rs +303 -0
- data/src/lib.rs +578 -0
- data/src/load_tokenizer/hf.rs +1036 -0
- data/src/load_tokenizer/hub.rs +344 -0
- data/src/load_tokenizer/mod.rs +3 -0
- data/src/load_tokenizer/tiktoken.rs +87 -0
- data/src/main.rs +95 -0
- data/src/pretokenize/fast/cl100k.rs +426 -0
- data/src/pretokenize/fast/cl100k_family.rs +891 -0
- data/src/pretokenize/fast/deepseek_v3.rs +605 -0
- data/src/pretokenize/fast/kimi.rs +281 -0
- data/src/pretokenize/fast/mask.rs +1486 -0
- data/src/pretokenize/fast/mod.rs +446 -0
- data/src/pretokenize/fast/nemotron.rs +138 -0
- data/src/pretokenize/fast/o200k.rs +347 -0
- data/src/pretokenize/fast/o200k_family.rs +1734 -0
- data/src/pretokenize/fast/olmo3.rs +505 -0
- data/src/pretokenize/fast/qwen2.rs +429 -0
- data/src/pretokenize/fast/qwen3_5.rs +541 -0
- data/src/pretokenize/fast/r50k.rs +1250 -0
- data/src/pretokenize/mod.rs +1079 -0
- data/src/pretokenize/options.rs +188 -0
- data/src/pretokenize/pretoken.rs +20 -0
- data/src/pretokenize/pretokenize_traits.rs +49 -0
- data/src/pretokenize/reference/avx512.rs +522 -0
- data/src/pretokenize/reference/combinator.rs +572 -0
- data/src/pretokenize/reference/mod.rs +28 -0
- data/src/pretokenize/reference/simd.rs +852 -0
- data/src/pretokenize/reference/state_machine.rs +365 -0
- data/src/pretokenize/unicode.rs +546 -0
- data/src/test_hub.rs +28 -0
- data/src/token.rs +42 -0
- metadata +161 -0
|
@@ -0,0 +1,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
|
+
}
|