1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
use super::super::types::{Guard, RunSplit, Tokenizer};
/// SentencePiece's metaspace marker, `U+2581`. Spelled once: it is three bytes
/// wide, and the split below steps by exactly that.
pub(in crate::core::tokenizer) const MARKER: &str = "▁";
#[cfg(feature = "rayon")]
use rayon::prelude::*;
impl Tokenizer {
/// Canonical content-encoding pipeline: normalizer, then the pre-tokenizer
/// / metaspace / plain-chunk fork.
///
/// `parallel` affects the plain-chunk fork (via [`Tokenizer::map_chunks`])
/// and the pre-tokenizer fork, whose pieces are independent of each other
/// once the engine has produced them.
///
/// The metaspace-decoder fork stays sequential regardless: it accumulates
/// `pending_underscores` as a strictly left-to-right fold over chunks, so it
/// is genuinely stateful and cannot be parallelized without changing output.
pub(super) fn encode_content(&self, text: &str, parallel: bool) -> Vec<u32> {
let mut out = Vec::new();
self.encode_content_into(text, parallel, true, &mut out);
out
}
/// [`Tokenizer::encode_content`] appending to a buffer the caller owns.
///
/// This is what added-token dispatch calls, once per gap between special
/// tokens — so on text those are dense in, a returned `Vec` per gap is an
/// allocation, a copy and a free for a handful of ids. The streaming
/// pre-tokenizer paths write straight through; the rest keep their own
/// buffer and are copied over, which is what they did before.
///
/// `is_first` says whether this gap opens the input, which `Metaspace`'s
/// `prepend_scheme: "first"` needs and nothing else reads: HuggingFace
/// prepends the replacement to the sequence's first split only, so a gap
/// that follows an added token does not get one. A gap that follows an added
/// token is never the first split, and neither is anything after it — which
/// is exactly what the pointer test at the call sites reports.
pub(super) fn encode_content_into(
&self,
text: &str,
parallel: bool,
is_first: bool,
out: &mut Vec<u32>,
) {
// Apply the HF `normalizer` (e.g. NFC) to content before splitting. This
// runs on content gaps (special tokens are extracted upstream), matching
// HuggingFace's extract-then-normalize order.
let normalized = self.normalized(text);
let text = normalized.as_ref();
// Multi-stage pre-tokenizer path (Digits/Punctuation/Sequence/…): the
// engine produces already byte-level-encoded pieces; BPE each directly.
//
// `split_pieces` rather than `split`: the pieces are consumed
// immediately and never stored, so materializing a `String` for each
// one is pure waste on the path every `tokenizer.json` model takes.
if let Some(pt) = &self.pre_tokenizer {
#[cfg(feature = "rayon")]
if parallel {
// Splitting into a `Vec` first is what lets the pieces be shared
// out across threads; the streaming path below cannot, since it
// hands back one reused buffer.
//
// Unmapped when the merge works from raw bytes — the same
// choice the sequential path makes below, and it has to be the
// same one: both feed the chunk cache, which is keyed by the
// bytes it is handed and cannot hold two spaces at once.
let pieces = match self.merges_raw() {
true => pt.split_raw_pieces(text),
false => pt.split_pieces(text),
};
out.extend(
pieces
.par_iter()
.fold(Vec::new, |mut acc, piece| {
self.encode_chunk_into(piece.as_bytes(), &mut acc);
acc
})
.reduce(Vec::new, |mut a, b| {
a.extend_from_slice(&b);
a
}),
);
return;
}
// One id per pre-token is the floor, so sizing from the text holds
// the whole result without regrowing. The streaming path has no
// piece count to size from — that was the point of not building one
// — so it estimates from the same rule the pipeline uses.
out.reserve(crate::core::pretokenizer::estimated_pieces(text));
// When the pipeline ends in ByteLevel, take the pieces unmapped and
// let `encode_raw_chunk_into` map only the ones that need it.
if self.use_byte_level && self.raw_encoder.is_some() && pt.emits_raw() {
// `scratch` is only touched when the merge still needs the
// ByteLevel form; a tokenizer that merges raw never fills it.
// Sized rather than grown from empty. It is cleared and refilled
// per piece, so it settles at the longest pre-token in the text
// — but it reaches that by doubling from zero on the first few
// pieces, and on macOS each of those regrows is a fresh
// allocation plus a copy. 64 bytes covers a pre-token in every
// script the bundled vocabularies cover; a longer one still
// grows, exactly as before.
// Empty when the merge works from raw bytes, which never asks
// for the mapping: `String::new` does not allocate, and this
// runs once per gap.
let mut scratch = match self.merges_raw() {
true => String::new(),
false => String::with_capacity(64),
};
pt.for_each_raw_piece(text, |piece| {
self.encode_raw_chunk_into(piece.as_bytes(), out, &mut scratch)
});
return;
}
pt.for_each_piece(text, |piece| self.encode_chunk_into(piece.as_bytes(), out));
return;
}
if self.use_metaspace_decoder {
return crate::core::scratch::with_text(|buf| {
self.metaspace_transform_at(text, is_first, buf);
self.for_each_metaspace_piece(buf, |piece| {
self.encode_chunk_into(piece.as_bytes(), out)
});
});
}
let text = self.prefixed(text);
let text = text.as_ref();
let text_bytes = text.as_bytes();
// The spans are built, walked and dropped inside this call, which is
// exactly the shape a per-thread buffer serves: the vector outlives the
// encode and is reused by the next one on this thread. Nothing escapes
// — both arms return ids, never a borrow of the spans.
crate::core::scratch::with_spans(|chunks| {
self.split_chunks_into(text, chunks);
if chunks.is_empty() {
return;
}
out.extend(self.map_chunks(text_bytes, chunks, parallel));
})
}
/// Write `text` as HuggingFace's `Metaspace` node leaves it: every **space**
/// (U+0020, and nothing else that is whitespace) becomes `▁`, then a leading
/// `▁` is prepended unless the result already opens with one.
///
/// Both halves are literal readings of `Metaspace::pre_tokenize`, and both
/// matter. Replacing anything wider than a space would eat a tab or a
/// newline the vocabulary spells with its own byte token; guarding the
/// prepend on the *replaced* text rather than the input is what makes
/// `" a"` and `"▁a"` agree, while `"\ta"` still takes the prefix.
pub(in crate::core::tokenizer) fn metaspace_transform(&self, text: &str, out: &mut String) {
self.metaspace_transform_at(text, true, out)
}
/// [`Tokenizer::metaspace_transform`] told whether this text opens the
/// sequence.
///
/// `prepend_scheme: "first"` — every metaspace vocabulary here — prepends to
/// the first split and no other, so a content gap that follows an added
/// token is transformed without a prefix. Measured on mistral-7b-v0.3:
/// `"([0-5]"` is `['▁(', '[', …]` while `"<s>([0-5]"` is `['<s>', '([', …]`,
/// which is a different first token, not merely a missing one.
pub(in crate::core::tokenizer) fn metaspace_transform_at(
&self,
text: &str,
is_first: bool,
out: &mut String,
) {
// Every space grows by two bytes, so a text that is all spaces triples.
// Sizing for the text plus a marker covers ordinary prose in one
// allocation and leaves the pathological case to grow, which is what a
// per-thread buffer settles into anyway.
out.reserve(text.len() + MARKER.len());
// The prefix is decided from the INPUT rather than inserted into the
// finished buffer: `String::insert(0, ..)` shifts every byte already
// written, which on a whole document is a memmove the size of the
// document. `out` starts with the marker exactly when the text starts
// with a space (which becomes one) or with a marker already.
if self.add_prefix_space && is_first && !text.starts_with(' ') && !text.starts_with(MARKER)
{
out.push_str(MARKER);
}
// Copy the runs between spaces wholesale. Pushing one `char` at a time
// re-encodes every character of the document through `char::encode_utf8`
// for the one byte value that changes.
let bytes = text.as_bytes();
let mut start = 0;
for at in memchr::memchr_iter(b' ', bytes) {
out.push_str(&text[start..at]);
out.push_str(MARKER);
start = at + 1;
}
out.push_str(&text[start..]);
}
/// Run `f` on every piece a `Metaspace` node splits its (already
/// transformed) text into.
///
/// With `split: false` that is the whole text, once — which is the shape
/// Mistral ships, and the reason this cannot be a whitespace regex: a merge
/// there may legitimately span what looks like a word boundary.
///
/// With `split: true` it is HuggingFace's `MergedWithNext` split on the
/// replacement, so each `▁` opens a piece and carries into the word behind
/// it. Text before the first `▁` is a piece of its own.
///
/// The `split: false` case still cuts when
/// [`metaspace_runs_are_boundaries`](Self::metaspace_runs_are_boundaries)
/// proves the cuts change nothing — at the start of each marker RUN, never
/// inside one, which is the weaker split that proof licenses.
pub(in crate::core::tokenizer) fn for_each_metaspace_piece(
&self,
text: &str,
mut f: impl FnMut(&str),
) {
// Declared `split: true` cuts at every marker; a proven split cuts only
// at run starts, and only where no token spans the cut — which for most
// vocabularies is everywhere, so the guard check is skipped entirely
// rather than asked per marker.
let (run_starts_only, guarded) = match self.metaspace_split {
true => (false, None),
false => match self.marker_run_split() {
RunSplit::Never => {
if !text.is_empty() {
f(text);
}
return;
}
split @ RunSplit::Allowed { guards, .. } => {
(true, (!guards.is_empty()).then_some(split))
}
},
};
let mut start = 0;
let mut prev_end = usize::MAX;
// `match_indices` rather than `split`: the delimiter stays with the
// piece that follows it, so the boundaries are what is wanted, not the
// fragments between them.
for (at, _) in text.match_indices(MARKER) {
// A marker directly behind this one means we are mid-run, and a run
// may merge into a `▁▁`-style token — so only its first marker is a
// boundary when the split was proven rather than declared.
let mid_run = run_starts_only && at == prev_end;
prev_end = at + MARKER.len();
if mid_run {
continue;
}
if let Some(split) = guarded {
if at > 0 && !split.cuts_at(text.as_bytes(), at) {
continue;
}
}
if at > start {
f(&text[start..at]);
}
start = at;
}
if start < text.len() {
f(&text[start..]);
}
}
/// Where this vocabulary's own tokens allow a chunk to be cut at the start
/// of a marker run. See [`RunSplit`] for why such a cut can be free.
///
/// A token can only span a run start by carrying a marker that is neither
/// its first character nor part of its leading run — `…x▁…`. Every such
/// token becomes a [`Guard`]; a cut is taken unless a guard sits across it.
/// mistral-7b-v0.3 yields none at all (of 32,768 tokens exactly 14 hold an
/// interior marker and every one is a pure run), Gemma yields the single
/// `>▁</`.
///
/// Beyond [`MAX_GUARDS`] the vocabulary is not really metaspace-shaped and
/// the per-cut check would cost more than the merge it saves, so nothing is
/// cut. A vocabulary with no marker token at all is not metaspace at all.
///
/// Proven once per tokenizer, on first encode.
pub(in crate::core::tokenizer) fn marker_run_split(&self) -> &RunSplit {
self.metaspace_run_split.get_or_init(|| {
let mut guards: Vec<Guard> = Vec::new();
let mut marker_tokens = false;
for token in self.encoder.keys() {
// Past the leading run: a marker there is the token's own
// first character and spans nothing.
let mut pos = leading_run_len(token);
marker_tokens |= pos > 0;
while let Some(off) = memchr::memmem::find(&token[pos..], MARKER.as_bytes()) {
if guards.len() == MAX_GUARDS {
return RunSplit::Never;
}
let at = pos + off;
guards.push(Guard {
token: token.into(),
at,
});
// Only a run's *start* is ever a cut, so step over the rest
// of this one before looking for the next.
pos = at + leading_run_len(&token[at..]);
}
}
if !marker_tokens {
return RunSplit::Never;
}
let mut preceded_by = Box::new([false; 256]);
for guard in &guards {
preceded_by[guard.token[guard.at - 1] as usize] = true;
}
RunSplit::Allowed {
guards: guards.into(),
preceded_by,
}
})
}
}
/// How many tokens may span a marker-run start before the cut is abandoned.
const MAX_GUARDS: usize = 8;
/// Length of the run of markers `token` opens with, in bytes.
fn leading_run_len(token: &[u8]) -> usize {
let mut len = 0;
while token[len..].starts_with(MARKER.as_bytes()) {
len += MARKER.len();
}
len
}
impl RunSplit {
/// Whether cutting `chunk` at `pos` — the start of a marker run — leaves
/// every id unchanged.
///
/// Free unless a guard token straddles the cut. The byte before the cut
/// answers that for almost every position without touching the list, and
/// for the vocabularies with no guard at all it answers it for every one.
#[inline]
pub(in crate::core::tokenizer) fn cuts_at(&self, chunk: &[u8], pos: usize) -> bool {
let RunSplit::Allowed {
guards,
preceded_by,
} = self
else {
return false;
};
// Most metaspace vocabularies hold no token that can span a cut at all,
// and then there is nothing to look up.
if guards.is_empty() {
return true;
}
if !preceded_by[chunk[pos - 1] as usize] {
return true;
}
!guards.iter().any(|guard| {
let Some(from) = pos.checked_sub(guard.at) else {
return false;
};
chunk[from..].starts_with(&guard.token)
})
}
}