1pub mod cxx;
14pub mod pre;
15pub mod regex;
16pub mod split;
17pub mod unicode;
18mod unicode_tables;
19
20use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap};
21use std::fmt;
22
23use super::gguf::{Gguf, MetaValue};
24use pre::{PreSettings, PreType};
25use regex::RegexError;
26use split::Splitter;
27use unicode::InvalidCodepoint;
28
29pub mod attr {
31 pub const UNDEFINED: u32 = 0;
32 pub const UNKNOWN: u32 = 1 << 0;
33 pub const UNUSED: u32 = 1 << 1;
34 pub const NORMAL: u32 = 1 << 2;
35 pub const CONTROL: u32 = 1 << 3;
36 pub const USER_DEFINED: u32 = 1 << 4;
37 pub const BYTE: u32 = 1 << 5;
38 pub const NORMALIZED: u32 = 1 << 6;
39 pub const LSTRIP: u32 = 1 << 7;
40 pub const RSTRIP: u32 = 1 << 8;
41 pub const SINGLE_WORD: u32 = 1 << 9;
42}
43
44const ESCAPED_SPACE: &[u8] = "\u{2581}".as_bytes();
46const DNA_KMER_MARKER: &[u8] = b"\xee\x80\x80";
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum VocabKind {
52 Spm,
54 Bpe(PreType),
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60enum BpeSession {
61 Plain,
62 HybridDna,
63 Whitespace,
64}
65
66#[derive(Clone, Debug, PartialEq)]
68pub enum VocabError {
69 Missing(&'static str),
70 WrongType(&'static str),
71 UnknownModel(String),
72 UnsupportedModel(String),
73 UnknownPre(String),
74 TooFewScores { scores: usize, tokens: usize },
75 TooFewTypes { types: usize, tokens: usize },
76 DuplicateToken(Vec<u8>),
77 Regex(RegexError),
78 NoNewline,
79}
80
81impl fmt::Display for VocabError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 VocabError::Missing(k) => write!(f, "the vocabulary has no `{k}`"),
85 VocabError::WrongType(k) => write!(f, "`{k}` has the wrong GGUF type"),
86 VocabError::UnknownModel(m) => write!(f, "unknown tokenizer model `{m}`"),
87 VocabError::UnsupportedModel(m) => {
88 write!(f, "the `{m}` tokenizer is not run here (byte-pair and SentencePiece vocabularies are)")
89 }
90 VocabError::UnknownPre(p) => write!(f, "unknown pre-tokenizer `{p}`"),
91 VocabError::TooFewScores { scores, tokens } => write!(f, "{scores} scores for {tokens} tokens"),
92 VocabError::TooFewTypes { types, tokens } => write!(f, "{types} token types for {tokens} tokens"),
93 VocabError::DuplicateToken(t) => write!(f, "the token {:?} appears twice", String::from_utf8_lossy(t)),
94 VocabError::Regex(e) => write!(f, "{e}"),
95 VocabError::NoNewline => write!(f, "the SentencePiece vocabulary has no newline token"),
96 }
97 }
98}
99
100impl std::error::Error for VocabError {}
101
102#[derive(Clone, Debug, PartialEq, Eq)]
104pub enum TokenizeError {
105 InvalidCodepoint(u32),
107 MissingByteToken(u8),
109 MissingSpecial(&'static str),
111}
112
113impl fmt::Display for TokenizeError {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 match self {
116 TokenizeError::InvalidCodepoint(c) => write!(f, "the text decodes to U+{c:X}, past U+10FFFF"),
117 TokenizeError::MissingByteToken(b) => write!(f, "the vocabulary has no token for byte 0x{b:02X}"),
118 TokenizeError::MissingSpecial(s) => write!(f, "the vocabulary has no {s} token"),
119 }
120 }
121}
122
123impl std::error::Error for TokenizeError {}
124
125impl From<InvalidCodepoint> for TokenizeError {
126 fn from(e: InvalidCodepoint) -> Self {
127 TokenizeError::InvalidCodepoint(e.0)
128 }
129}
130
131#[derive(Clone, Debug, PartialEq)]
133pub struct TokenData {
134 pub text: Vec<u8>,
135 pub score: f32,
136 pub attr: u32,
137}
138
139#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
141pub struct Specials {
142 pub bos: Option<u32>,
143 pub eos: Option<u32>,
144 pub eot: Option<u32>,
145 pub eom: Option<u32>,
146 pub unk: Option<u32>,
147 pub sep: Option<u32>,
148 pub pad: Option<u32>,
149 pub mask: Option<u32>,
150 pub fim_pre: Option<u32>,
151 pub fim_suf: Option<u32>,
152 pub fim_mid: Option<u32>,
153 pub fim_pad: Option<u32>,
154 pub fim_rep: Option<u32>,
155 pub fim_sep: Option<u32>,
156}
157
158#[derive(Clone, Debug)]
160pub struct Vocab {
161 pub kind: VocabKind,
162 pub model: String,
163 pub specials: Specials,
164 pub linefeed: Option<u32>,
165 pub add_space_prefix: bool,
166 pub add_bos: bool,
167 pub add_eos: bool,
168 pub ignore_merges: bool,
169 pub clean_spaces: bool,
170 pub escape_whitespaces: bool,
171 pub treat_whitespace_as_suffix: bool,
172 pub lowercase: bool,
173 tokens: Vec<TokenData>,
174 token_to_id: HashMap<Vec<u8>, u32>,
175 bpe_ranks: HashMap<Vec<u8>, u32>,
176 eog: BTreeSet<u32>,
177 special_cache: Vec<u32>,
178 piece_cache: Vec<Vec<u8>>,
179 splitter: Option<Splitter>,
180 session: BpeSession,
181}
182
183fn pair_key(first: &[u8], second: &[u8]) -> Vec<u8> {
185 let mut k = Vec::with_capacity(4 + first.len() + second.len());
186 k.extend_from_slice(&(first.len() as u32).to_le_bytes());
187 k.extend_from_slice(first);
188 k.extend_from_slice(second);
189 k
190}
191
192fn meta_str<'a>(meta: &'a BTreeMap<String, MetaValue>, key: &str) -> Option<&'a str> {
193 meta.get(key).and_then(MetaValue::as_str)
194}
195
196fn find(hay: &[u8], needle: &[u8], from: usize) -> Option<usize> {
198 if needle.is_empty() {
199 return (from <= hay.len()).then_some(from);
200 }
201 if from > hay.len() || hay.len() - from < needle.len() {
202 return None;
203 }
204 hay[from..].windows(needle.len()).position(|w| w == needle).map(|p| p + from)
205}
206
207fn replace_all(s: &[u8], search: &[u8], replace: &[u8]) -> Vec<u8> {
209 let mut out = Vec::with_capacity(s.len());
210 let mut last = 0;
211 while let Some(p) = find(s, search, last) {
212 out.extend_from_slice(&s[last..p]);
213 out.extend_from_slice(replace);
214 last = p + search.len();
215 }
216 out.extend_from_slice(&s[last..]);
217 out
218}
219
220fn c_isspace(b: u8) -> bool {
221 matches!(b, b' ' | b'\t' | b'\n' | 0x0B | 0x0C | b'\r')
222}
223
224#[derive(Clone, Copy, Debug)]
226enum Fragment {
227 Token(u32),
228 Raw { offset: usize, len: usize },
229}
230
231impl Vocab {
232 pub fn from_gguf(g: &Gguf) -> Result<Vocab, VocabError> {
234 Vocab::from_meta(&g.meta)
235 }
236
237 pub fn from_meta(meta: &BTreeMap<String, MetaValue>) -> Result<Vocab, VocabError> {
240 let model = meta_str(meta, "tokenizer.ggml.model")
241 .ok_or(VocabError::Missing("tokenizer.ggml.model"))?
242 .to_string();
243 let mut pre_name = meta_str(meta, "tokenizer.ggml.pre").map(str::to_string);
244 let mut specials = Specials::default();
245 let mut kind_spm = false;
246 let mut bpe_ranks = HashMap::new();
247 let mut session = BpeSession::Plain;
248 let read_merges = |ranks: &mut HashMap<Vec<u8>, u32>| -> Result<(), VocabError> {
249 let merges = meta
250 .get("tokenizer.ggml.merges")
251 .ok_or(VocabError::Missing("tokenizer.ggml.merges"))?
252 .as_array()
253 .ok_or(VocabError::WrongType("tokenizer.ggml.merges"))?;
254 for (i, m) in merges.iter().enumerate() {
255 let word = m.as_bytes().ok_or(VocabError::WrongType("tokenizer.ggml.merges"))?;
256 let (first, second): (&[u8], &[u8]) = match find(word, b" ", 1) {
257 Some(p) => (&word[..p], &word[p + 1..]),
258 None => (b"", b""),
259 };
260 ranks.entry(pair_key(first, second)).or_insert(i as u32);
261 }
262 Ok(())
263 };
264 match model.as_str() {
265 "llama" => {
266 kind_spm = true;
267 specials.bos = Some(1);
268 specials.eos = Some(2);
269 specials.unk = Some(0);
270 }
271 "gpt2" | "hybriddna" | "whitespace" => {
272 if pre_name.as_deref() != Some("kimi-k2") || meta.contains_key("tokenizer.ggml.merges") {
273 read_merges(&mut bpe_ranks)?;
274 }
275 specials.bos = Some(11);
276 specials.eos = Some(11);
277 session = match model.as_str() {
278 "hybriddna" => BpeSession::HybridDna,
279 "whitespace" => BpeSession::Whitespace,
280 _ => BpeSession::Plain,
281 };
282 }
283 "gemma4" => {
284 read_merges(&mut bpe_ranks)?;
285 pre_name = Some("gemma4".to_string());
286 }
287 "bert" | "t5" | "rwkv" | "plamo2" | "plamo3" | "test" | "no_vocab" | "none" => {
288 return Err(VocabError::UnsupportedModel(model));
289 }
290 other => return Err(VocabError::UnknownModel(other.to_string())),
291 }
292
293 let (kind, mut add_space_prefix, add_bos, add_eos, ignore_merges, clean_spaces, escape_whitespaces, lowercase_cleared) =
294 if kind_spm {
295 (VocabKind::Spm, true, true, false, false, false, true, false)
296 } else {
297 let (pt, set): (PreType, PreSettings) = match pre_name.as_deref() {
298 None => (PreType::Default, PreSettings::default()),
299 Some(p) => pre::pre_type(p).ok_or_else(|| VocabError::UnknownPre(p.to_string()))?,
300 };
301 if set.no_bos_token {
302 specials.bos = None;
303 }
304 (
305 VocabKind::Bpe(pt),
306 false,
307 set.add_bos.unwrap_or(false),
308 false,
309 set.ignore_merges.unwrap_or(false),
310 set.clean_spaces.unwrap_or(true),
311 set.escape_whitespaces.unwrap_or(false),
312 set.no_lowercase,
313 )
314 };
315 let get_bool = |k: &'static str| -> Result<Option<bool>, VocabError> {
316 match meta.get(k) {
317 None => Ok(None),
318 Some(v) => v.as_bool().map(Some).ok_or(VocabError::WrongType(k)),
319 }
320 };
321 if let Some(v) = get_bool("tokenizer.ggml.add_space_prefix")? {
322 add_space_prefix = v;
323 }
324
325 let list = meta
326 .get("tokenizer.ggml.tokens")
327 .ok_or(VocabError::Missing("tokenizer.ggml.tokens"))?
328 .as_array()
329 .ok_or(VocabError::WrongType("tokenizer.ggml.tokens"))?;
330 let n = list.len();
331 let scores: Option<Vec<f32>> = match meta.get("tokenizer.ggml.scores") {
332 None => None,
333 Some(v) => {
334 let a = v.as_array().ok_or(VocabError::WrongType("tokenizer.ggml.scores"))?;
335 if a.len() < n {
336 return Err(VocabError::TooFewScores { scores: a.len(), tokens: n });
337 }
338 Some(
339 a.iter()
340 .map(|x| match *x {
341 MetaValue::F32(f) => Ok(f),
342 MetaValue::I32(i) => Ok(i as f32),
343 _ => Err(VocabError::WrongType("tokenizer.ggml.scores")),
344 })
345 .collect::<Result<_, _>>()?,
346 )
347 }
348 };
349 let types: Option<Vec<i32>> = match meta.get("tokenizer.ggml.token_type") {
350 None => None,
351 Some(v) => {
352 let a = v.as_array().ok_or(VocabError::WrongType("tokenizer.ggml.token_type"))?;
353 if a.len() < n {
354 return Err(VocabError::TooFewTypes { types: a.len(), tokens: n });
355 }
356 Some(
357 a.iter()
358 .map(|x| match *x {
359 MetaValue::I32(i) => Ok(i),
360 _ => Err(VocabError::WrongType("tokenizer.ggml.token_type")),
361 })
362 .collect::<Result<_, _>>()?,
363 )
364 }
365 };
366 let mut tokens = Vec::with_capacity(n);
367 let mut token_to_id = HashMap::with_capacity(n);
368 for (i, t) in list.iter().enumerate() {
369 let mut text = t.as_bytes().ok_or(VocabError::WrongType("tokenizer.ggml.tokens"))?.to_vec();
370 if text.is_empty() {
371 text = format!("[EMPTY_{i}]").into_bytes();
372 }
373 if token_to_id.insert(text.clone(), i as u32).is_some() {
374 return Err(VocabError::DuplicateToken(text));
375 }
376 let attr = match types.as_ref().map(|t| t[i]) {
377 None => attr::NORMAL,
378 Some(0) => attr::UNDEFINED,
379 Some(1) => attr::NORMAL,
380 Some(2) => attr::UNKNOWN,
381 Some(3) => attr::CONTROL,
382 Some(4) => attr::USER_DEFINED,
383 Some(5) => attr::UNUSED,
384 Some(6) => attr::BYTE,
385 Some(_) => attr::UNDEFINED,
386 };
387 tokens.push(TokenData { text, score: scores.as_ref().map_or(0.0, |s| s[i]), attr });
388 }
389 let map_hashes: Vec<u64> = tokens.iter().map(|t: &TokenData| cxx::hash_string(&t.text)).collect();
390 if model == "hybriddna" {
391 if let Some(&oov) = token_to_id.get(b"<oov>".as_slice()) {
392 for t in tokens.iter_mut().skip(oov as usize + 1) {
393 if t.text.len() > DNA_KMER_MARKER.len() && t.text.ends_with(DNA_KMER_MARKER) {
394 let keep = t.text.len() - DNA_KMER_MARKER.len();
395 t.text.truncate(keep);
396 }
397 }
398 }
399 }
400
401 let splitter = match kind {
402 VocabKind::Bpe(pt) => Some(Splitter::new(pt.regexes(), pt.byte_encode()).map_err(VocabError::Regex)?),
403 VocabKind::Spm => None,
404 };
405 let mut v = Vocab {
406 kind,
407 model,
408 specials,
409 linefeed: None,
410 add_space_prefix,
411 add_bos,
412 add_eos,
413 ignore_merges,
414 clean_spaces,
415 escape_whitespaces,
416 treat_whitespace_as_suffix: false,
417 lowercase: true,
418 tokens,
419 token_to_id,
420 bpe_ranks,
421 eog: BTreeSet::new(),
422 special_cache: Vec::new(),
423 piece_cache: Vec::new(),
424 splitter,
425 session,
426 };
427
428 v.linefeed = match kind {
429 VocabKind::Spm => Some(v.byte_to_token(b'\n').map_err(|_| VocabError::NoNewline)?),
430 VocabKind::Bpe(_) => v.tokenize(b"\n", false, false).ok().and_then(|ids| ids.first().copied()),
431 };
432
433 let n_tokens = v.tokens.len();
434 let in_range = |id: Option<u32>| id.filter(|&i| (i as usize) < n_tokens);
435 for slot in [&mut v.specials.bos, &mut v.specials.eos, &mut v.specials.unk] {
436 *slot = in_range(*slot);
437 }
438 let keys: [(&'static str, fn(&mut Specials) -> &mut Option<u32>); 17] = [
439 ("tokenizer.ggml.bos_token_id", |s| &mut s.bos),
440 ("tokenizer.ggml.eos_token_id", |s| &mut s.eos),
441 ("tokenizer.ggml.eot_token_id", |s| &mut s.eot),
442 ("tokenizer.ggml.eom_token_id", |s| &mut s.eom),
443 ("tokenizer.ggml.unknown_token_id", |s| &mut s.unk),
444 ("tokenizer.ggml.seperator_token_id", |s| &mut s.sep),
445 ("tokenizer.ggml.padding_token_id", |s| &mut s.pad),
446 ("tokenizer.ggml.mask_token_id", |s| &mut s.mask),
447 ("tokenizer.ggml.fim_pre_token_id", |s| &mut s.fim_pre),
448 ("tokenizer.ggml.fim_suf_token_id", |s| &mut s.fim_suf),
449 ("tokenizer.ggml.fim_mid_token_id", |s| &mut s.fim_mid),
450 ("tokenizer.ggml.fim_pad_token_id", |s| &mut s.fim_pad),
451 ("tokenizer.ggml.fim_rep_token_id", |s| &mut s.fim_rep),
452 ("tokenizer.ggml.fim_sep_token_id", |s| &mut s.fim_sep),
453 ("tokenizer.ggml.prefix_token_id", |s| &mut s.fim_pre),
454 ("tokenizer.ggml.suffix_token_id", |s| &mut s.fim_suf),
455 ("tokenizer.ggml.middle_token_id", |s| &mut s.fim_mid),
456 ];
457 for (key, slot) in keys {
458 let Some(val) = meta.get(key) else { continue };
459 let MetaValue::U32(id) = *val else { return Err(VocabError::WrongType(key)) };
460 if (id as usize) < n_tokens {
461 *slot(&mut v.specials) = Some(id);
462 }
463 }
464 if let Some(b) = get_bool("tokenizer.ggml.add_bos_token")? {
465 v.add_bos = b;
466 }
467 if let Some(b) = get_bool("tokenizer.ggml.add_eos_token")? {
468 v.add_eos = b;
469 }
470 if v.kind == VocabKind::Bpe(PreType::Gemma4) && !v.add_bos {
471 v.add_bos = true;
472 }
473 v.lowercase = !lowercase_cleared;
474 if let Some(b) = get_bool("tokenizer.ggml.normalizer.lowercase")? {
475 v.lowercase = b;
476 }
477
478 v.detect_specials(&map_hashes);
479 v.build_special_cache();
480 v.piece_cache = (0..n_tokens as u32).map(|id| v.piece_uncached(id)).collect();
481 v.apply_name_attrs(meta);
482 Ok(v)
483 }
484
485 fn detect_specials(&mut self, map_hashes: &[u64]) {
489 type Slot = fn(&mut Specials) -> &mut Option<u32>;
490 let rules: [(Slot, &[&str]); 8] = [
491 (
492 |s| &mut s.eot,
493 &[
494 "<|eot_id|>",
495 "<|im_end|>",
496 "<|end|>",
497 "<end_of_turn>",
498 "<|endoftext|>",
499 "<|end_of_text|>",
500 "<EOT>",
501 "_<EOT>",
502 "[EOT]",
503 "<|end▁of▁sentence|>",
504 "<end_of_utterance>",
505 ],
506 ),
507 (|s| &mut s.eom, &["<|eom_id|>"]),
508 (
509 |s| &mut s.fim_pre,
510 &[
511 "<|fim_prefix|>",
512 "<fim-prefix>",
513 "<fim_prefix>",
514 "<|fim▁begin|>",
515 "<PRE>",
516 "▁<PRE>",
517 "<|code_prefix|>",
518 "<|prefix|>",
519 "<|plamo:fim_prefix|>",
520 ],
521 ),
522 (
523 |s| &mut s.fim_suf,
524 &[
525 "<|fim_suffix|>",
526 "<fim-suffix>",
527 "<fim_suffix>",
528 "<|fim▁hole|>",
529 "<SUF>",
530 "▁<SUF>",
531 "<|code_suffix|>",
532 "<|suffix|>",
533 "<|plamo:fim_suffix|>",
534 ],
535 ),
536 (
537 |s| &mut s.fim_mid,
538 &[
539 "<|fim_middle|>",
540 "<fim-middle>",
541 "<fim_middle>",
542 "<|fim▁end|>",
543 "<MID>",
544 "▁<MID>",
545 "<|code_middle|>",
546 "<|middle|>",
547 "<|plamo:fim_middle|>",
548 ],
549 ),
550 (|s| &mut s.fim_pad, &["<|fim_pad|>", "<fim-pad>", "<fim_pad>", "<PAD>", "[PAD]"]),
551 (|s| &mut s.fim_rep, &["<|fim_repo|>", "<|repo_name|>", "<fim-repo>", "<REPO>", "<reponame>"]),
552 (|s| &mut s.fim_sep, &["<|file_sep|>", "<|plamo:file_separator|>"]),
553 ];
554 let mut rank = vec![0u32; map_hashes.len()];
555 for (pos, id) in cxx::unordered_map_order(map_hashes).into_iter().enumerate() {
556 rank[id as usize] = pos as u32;
557 }
558 for (slot, texts) in rules {
559 if slot(&mut self.specials).is_some() {
560 continue;
561 }
562 let found = texts
563 .iter()
564 .filter_map(|t| self.token_to_id.get(t.as_bytes()).copied())
565 .min_by_key(|&id| rank[id as usize]);
566 if let Some(id) = found {
567 *slot(&mut self.specials) = Some(id);
568 self.tokens[id as usize].attr |= attr::CONTROL;
569 }
570 }
571 for t in &mut self.tokens {
572 if t.attr & attr::CONTROL != 0 && t.attr & attr::UNUSED == 0 && find(&t.text, b"unused", 0).is_some() {
573 t.attr |= attr::UNUSED;
574 }
575 }
576 let mut eog = BTreeSet::new();
577 for id in [self.specials.fim_pad, self.specials.fim_rep, self.specials.fim_sep].into_iter().flatten() {
578 eog.insert(id);
579 }
580 const EOG_TEXTS: [&str; 23] = [
581 "<|eot_id|>",
582 "<|im_end|>",
583 "<|end|>",
584 "<|return|>",
585 "<|call|>",
586 "<|flush|>",
587 "<|calls|>",
588 "<end_of_turn>",
589 "<|endoftext|>",
590 "</s>",
591 "<|eom_id|>",
592 "<EOT>",
593 "_<EOT>",
594 "[EOT]",
595 "[EOS]",
596 "<|end_of_text|>",
597 "<end_of_utterance>",
598 "<eos>",
599 "<turn|>",
600 "<|tool_response>",
601 "<|end▁of▁sentence|>",
602 "[e~[",
603 "<|ifm|im_end|>",
604 ];
605 for text in EOG_TEXTS {
606 if let Some(&id) = self.token_to_id.get(text.as_bytes()) {
607 eog.insert(id);
608 self.tokens[id as usize].attr |= attr::CONTROL;
609 }
610 }
611 for text in ["<|channel|>", "<|message|>", "<|start|>", "<|constrain|>"] {
612 if let Some(&id) = self.token_to_id.get(text.as_bytes()) {
613 self.tokens[id as usize].attr = attr::USER_DEFINED;
614 }
615 }
616 for id in [self.specials.eos, self.specials.eot, self.specials.eom].into_iter().flatten() {
617 eog.insert(id);
618 }
619 let find_eog = |tokens: &[TokenData], eog: &BTreeSet<u32>, text: &[u8]| {
620 eog.iter().copied().find(|&id| tokens.get(id as usize).is_some_and(|t| t.text == text))
621 };
622 if let Some(end_id) = find_eog(&self.tokens, &eog, b"<|end|>") {
623 let has = |t: &[u8]| find_eog(&self.tokens, &eog, t).is_some();
624 let has_call = has(b"<|call|>") || has(b"<|calls|>");
625 if (has(b"<|return|>") && has_call) || (has_call && has(b"<|flush|>")) {
626 eog.remove(&end_id);
627 self.tokens[end_id as usize].attr = attr::USER_DEFINED;
628 }
629 }
630 let has_marker = find_eog(&self.tokens, &eog, b"<|tool_response>").is_some()
631 || find_eog(&self.tokens, &eog, b"<|plamo:eos|>").is_some();
632 if let (true, Some(s_id)) = (has_marker, find_eog(&self.tokens, &eog, b"</s>")) {
633 eog.remove(&s_id);
634 self.tokens[s_id as usize].attr = attr::NORMAL;
635 }
636 self.eog = eog;
637 }
638
639 fn build_special_cache(&mut self) {
640 let mut cache: Vec<u32> = (0..self.tokens.len() as u32)
641 .filter(|&id| self.tokens[id as usize].attr & (attr::CONTROL | attr::USER_DEFINED | attr::UNKNOWN) != 0)
642 .collect();
643 let tokens = &self.tokens;
644 cxx::sort(&mut cache, &|&a: &u32, &b: &u32| tokens[a as usize].text.len() > tokens[b as usize].text.len());
645 self.special_cache = cache;
646 }
647
648 fn apply_name_attrs(&mut self, meta: &BTreeMap<String, MetaValue>) {
650 let name = meta_str(meta, "general.name").unwrap_or("").to_ascii_lowercase();
651 let pre = meta_str(meta, "tokenizer.ggml.pre").unwrap_or("");
652 let arch = meta_str(meta, "general.architecture").unwrap_or("");
653 let any = |s: &str, subs: &[&str]| subs.iter().any(|x| s.contains(x));
654 let set = |v: &mut Vocab, text: &[u8], a: u32, on: bool| {
655 if let Some(&id) = v.token_to_id.get(text) {
656 let t = &mut v.tokens[id as usize];
657 t.attr = if on { t.attr | a } else { t.attr & !a };
658 }
659 };
660 if any(pre, &["jina-v2-de", "jina-v2-es", "jina-v2-code", "mmbert"]) || any(arch, &["nomic-bert-moe", "jina-bert-v3"]) {
661 set(self, b"<mask>", attr::LSTRIP, true);
662 } else if any(&name, &["phi-3", "phi3"]) {
663 for id in self.special_cache.clone() {
664 self.tokens[id as usize].attr |= attr::RSTRIP;
665 }
666 set(self, b"</s>", attr::RSTRIP, true);
667 for t in [b"<unk>".as_slice(), b"<s>", b"<|endoftext|>"] {
668 set(self, t, attr::RSTRIP, false);
669 }
670 } else if any(&name, &["modern-bert"]) {
671 set(self, b"[MASK]", attr::LSTRIP, true);
672 }
673 }
674
675 pub fn n_tokens(&self) -> usize {
676 self.tokens.len()
677 }
678
679 pub fn token(&self, id: u32) -> &TokenData {
680 &self.tokens[id as usize]
681 }
682
683 pub fn text_to_token(&self, text: &[u8]) -> Option<u32> {
684 self.token_to_id.get(text).copied()
685 }
686
687 pub fn is_eog(&self, id: u32) -> bool {
689 self.eog.contains(&id)
690 }
691
692 pub fn eog(&self) -> impl Iterator<Item = u32> + '_ {
694 self.eog.iter().copied()
695 }
696
697 pub fn special_order(&self) -> &[u32] {
699 &self.special_cache
700 }
701
702 fn bpe_rank(&self, first: &[u8], second: &[u8]) -> Option<u32> {
703 self.bpe_ranks.get(&pair_key(first, second)).copied()
704 }
705
706 pub fn byte_to_token(&self, b: u8) -> Result<u32, TokenizeError> {
708 const HEX: &[u8; 16] = b"0123456789ABCDEF";
709 let found = match self.kind {
710 VocabKind::Spm => {
711 let name = [b'<', b'0', b'x', HEX[(b >> 4) as usize], HEX[(b & 15) as usize], b'>'];
712 self.text_to_token(&name).or_else(|| {
713 let raw: &[u8] = if b == 0 { b"" } else { std::slice::from_ref(&b) };
714 self.text_to_token(raw)
715 })
716 }
717 VocabKind::Bpe(_) => {
718 let mut s = Vec::new();
719 unicode::byte_to_utf8(b, &mut s);
720 self.text_to_token(&s)
721 }
722 };
723 found.ok_or(TokenizeError::MissingByteToken(b))
724 }
725
726 fn token_to_byte(&self, id: u32) -> u8 {
728 let t = &self.tokens[id as usize].text;
729 let hex = t.get(3..t.len().min(5)).unwrap_or(&[]);
730 let mut v: u32 = 0;
731 for &c in hex {
732 match (c as char).to_digit(16) {
733 Some(d) => v = v * 16 + d,
734 None => break,
735 }
736 }
737 v as u8
738 }
739
740 fn partition(&self, text: &[u8], parse_special: bool) -> Vec<Fragment> {
742 let mut frags = vec![Fragment::Raw { offset: 0, len: text.len() }];
743 for &id in &self.special_cache {
744 let data = &self.tokens[id as usize];
745 if !parse_special && data.attr & (attr::CONTROL | attr::UNKNOWN) != 0 {
746 continue;
747 }
748 let needle = &data.text;
749 let mut next = Vec::with_capacity(frags.len());
750 for f in frags {
751 let Fragment::Raw { offset, len } = f else {
752 next.push(f);
753 continue;
754 };
755 let (mut base, mut blen) = (offset, len);
756 loop {
757 let Some(m) = find(&text[..base + blen], needle, base) else {
758 next.push(Fragment::Raw { offset: base, len: blen });
759 break;
760 };
761 if m > base {
762 let mut left = m - base;
763 if data.attr & attr::LSTRIP != 0 {
764 while left > 0 && c_isspace(text[base + left - 1]) {
765 left -= 1;
766 }
767 }
768 if left > 0 {
769 next.push(Fragment::Raw { offset: base, len: left });
770 }
771 }
772 next.push(Fragment::Token(id));
773 if m + needle.len() < base + blen {
774 let mut r_off = m + needle.len();
775 let mut r_len = blen - ((m - base) + needle.len());
776 if data.attr & attr::RSTRIP != 0 {
777 while r_len > 0 && c_isspace(text[r_off]) {
778 r_off += 1;
779 r_len -= 1;
780 }
781 }
782 if r_len == 0 {
783 break;
784 }
785 base = r_off;
786 blen = r_len;
787 } else {
788 break;
789 }
790 }
791 }
792 frags = next;
793 }
794 frags
795 }
796
797 pub fn tokenize(&self, text: &[u8], add_special: bool, parse_special: bool) -> Result<Vec<u32>, TokenizeError> {
800 let mut out = Vec::new();
801 let frags = if text.is_empty() { Vec::new() } else { self.partition(text, parse_special) };
802 match self.kind {
803 VocabKind::Spm => {
804 let mut prev_special = true;
805 if add_special && self.add_bos {
806 out.push(self.specials.bos.ok_or(TokenizeError::MissingSpecial("beginning-of-sequence"))?);
807 prev_special = true;
808 }
809 for f in &frags {
810 match *f {
811 Fragment::Raw { offset, len } => {
812 let mut t = Vec::with_capacity(len + 1);
813 if self.add_space_prefix && prev_special {
814 t.push(b' ');
815 }
816 t.extend_from_slice(&text[offset..offset + len]);
817 let t = replace_all(&t, b" ", ESCAPED_SPACE);
818 self.spm(&t, &mut out)?;
819 prev_special = false;
820 }
821 Fragment::Token(id) => {
822 out.push(id);
823 prev_special = true;
824 }
825 }
826 }
827 if add_special && self.add_eos {
828 out.push(self.specials.eos.ok_or(TokenizeError::MissingSpecial("end-of-sequence"))?);
829 }
830 }
831 VocabKind::Bpe(_) => {
832 if add_special && self.add_bos {
833 out.push(self.specials.bos.ok_or(TokenizeError::MissingSpecial("beginning-of-sequence"))?);
834 }
835 for f in &frags {
836 match *f {
837 Fragment::Raw { offset, len } => {
838 let raw = &text[offset..offset + len];
839 let t = if self.escape_whitespaces { replace_all(raw, b" ", ESCAPED_SPACE) } else { raw.to_vec() };
840 match self.session {
841 BpeSession::Plain => self.bpe(&t, &mut out)?,
842 BpeSession::HybridDna => self.bpe_dna(&t, &mut out)?,
843 BpeSession::Whitespace => self.bpe_whitespace(&t, &mut out)?,
844 }
845 }
846 Fragment::Token(id) => out.push(id),
847 }
848 }
849 if add_special && self.add_eos {
850 out.push(self.specials.eos.ok_or(TokenizeError::MissingSpecial("end-of-sequence"))?);
851 }
852 }
853 }
854 Ok(out)
855 }
856
857 fn spm(&self, text: &[u8], out: &mut Vec<u32>) -> Result<(), TokenizeError> {
859 #[derive(Clone, Copy)]
860 struct Sym {
861 prev: isize,
862 next: isize,
863 start: usize,
864 n: usize,
865 }
866 #[derive(PartialEq)]
867 struct Bigram {
868 score: f32,
869 left: usize,
870 right: usize,
871 size: usize,
872 }
873 impl Eq for Bigram {}
874 impl PartialOrd for Bigram {
875 fn partial_cmp(&self, o: &Self) -> Option<std::cmp::Ordering> {
876 Some(self.cmp(o))
877 }
878 }
879 impl Ord for Bigram {
880 fn cmp(&self, o: &Self) -> std::cmp::Ordering {
881 let less = |l: &Bigram, r: &Bigram| l.score < r.score || (l.score == r.score && l.left > r.left);
882 if less(self, o) {
883 std::cmp::Ordering::Less
884 } else if less(o, self) {
885 std::cmp::Ordering::Greater
886 } else {
887 std::cmp::Ordering::Equal
888 }
889 }
890 }
891 let mut syms: Vec<Sym> = Vec::new();
892 let mut offs = 0;
893 while offs < text.len() {
894 let n = unicode::len_utf8(text[offs]).min(text.len() - offs);
895 let idx = syms.len() as isize;
896 offs += n;
897 syms.push(Sym { prev: idx - 1, next: if offs == text.len() { -1 } else { idx + 1 }, start: offs - n, n });
898 }
899 let mut queue = BinaryHeap::new();
900 let try_add = |syms: &[Sym], queue: &mut BinaryHeap<Bigram>, left: isize, right: isize| {
901 if left < 0 || right < 0 {
902 return;
903 }
904 let (l, r) = (syms[left as usize], syms[right as usize]);
905 let piece = &text[l.start..l.start + l.n + r.n];
906 if let Some(id) = self.text_to_token(piece) {
907 queue.push(Bigram { score: self.tokens[id as usize].score, left: left as usize, right: right as usize, size: piece.len() });
908 }
909 };
910 for i in 1..syms.len() {
911 try_add(&syms, &mut queue, i as isize - 1, i as isize);
912 }
913 while let Some(b) = queue.pop() {
914 let (l, r) = (syms[b.left], syms[b.right]);
915 if l.n == 0 || r.n == 0 || l.n + r.n != b.size {
916 continue;
917 }
918 syms[b.left].n += r.n;
919 syms[b.right].n = 0;
920 syms[b.left].next = r.next;
921 if r.next >= 0 {
922 syms[r.next as usize].prev = b.left as isize;
923 }
924 let prev = syms[b.left].prev;
925 let next = syms[b.left].next;
926 try_add(&syms, &mut queue, prev, b.left as isize);
927 try_add(&syms, &mut queue, b.left as isize, next);
928 }
929 let mut i: isize = if syms.is_empty() { -1 } else { 0 };
930 while i != -1 {
931 let s = syms[i as usize];
932 let piece = &text[s.start..s.start + s.n];
933 match self.text_to_token(piece) {
935 Some(id) => out.push(id),
936 None => {
937 for &b in piece {
938 out.push(self.byte_to_token(b)?);
939 }
940 }
941 }
942 i = s.next;
943 }
944 Ok(())
945 }
946
947 fn bpe(&self, text: &[u8], out: &mut Vec<u32>) -> Result<(), TokenizeError> {
949 let VocabKind::Bpe(pre) = self.kind else { unreachable!("a byte-pair session runs a byte-pair vocabulary") };
950 let splitter = self.splitter.as_ref().expect("a byte-pair vocabulary has a splitter");
951 let mut words = splitter.split(text)?;
952 if pre == PreType::Mmbert {
953 let mut split = Vec::new();
954 for mut w in words {
955 if w.iter().all(|&c| c == b'\n') {
956 split.push(w);
957 continue;
958 }
959 if !w.starts_with(ESCAPED_SPACE) {
960 let mut p = ESCAPED_SPACE.to_vec();
961 p.extend_from_slice(&w);
962 w = p;
963 }
964 let mut start = 0;
965 while start < w.len() {
966 let end = find(&w, ESCAPED_SPACE, start + ESCAPED_SPACE.len()).unwrap_or(w.len());
967 split.push(w[start..end].to_vec());
968 start = end;
969 }
970 }
971 words = split;
972 }
973 let byte_encode = pre.byte_encode();
974 for word in &words {
975 for piece in self.bpe_word(word, pre) {
976 match self.text_to_token(piece) {
977 Some(id) => out.push(id),
978 None => {
979 for &b in piece {
980 let id = if byte_encode {
981 self.text_to_token(std::slice::from_ref(&b))
982 } else {
983 const HEX: &[u8; 16] = b"0123456789ABCDEF";
984 self.text_to_token(&[b'<', b'0', b'x', HEX[(b >> 4) as usize], HEX[(b & 15) as usize], b'>'])
985 };
986 if let Some(id) = id {
987 out.push(id);
988 }
989 }
990 }
991 }
992 }
993 }
994 Ok(())
995 }
996
997 fn bpe_word<'w>(&self, word: &'w [u8], pre: PreType) -> Vec<&'w [u8]> {
999 #[derive(Clone, Copy)]
1000 struct Sym {
1001 prev: isize,
1002 next: isize,
1003 start: usize,
1004 n: usize,
1005 }
1006 #[derive(PartialEq, Eq)]
1007 struct Bigram {
1008 rank: u32,
1009 left: usize,
1010 right: usize,
1011 text: Vec<u8>,
1012 }
1013 impl PartialOrd for Bigram {
1014 fn partial_cmp(&self, o: &Self) -> Option<std::cmp::Ordering> {
1015 Some(self.cmp(o))
1016 }
1017 }
1018 impl Ord for Bigram {
1019 fn cmp(&self, o: &Self) -> std::cmp::Ordering {
1020 (o.rank, o.left).cmp(&(self.rank, self.left))
1021 }
1022 }
1023 let mut syms: Vec<Sym> = Vec::new();
1024 let mut offset = 0;
1025 if self.ignore_merges && self.text_to_token(word).is_some() {
1026 syms.push(Sym { prev: -1, next: -1, start: 0, n: word.len() });
1027 offset = word.len();
1028 } else if matches!(pre, PreType::Gemma4 | PreType::Mmbert)
1029 && word.iter().all(|&c| c == b'\n')
1030 && self.text_to_token(word).is_some()
1031 {
1032 syms.push(Sym { prev: -1, next: -1, start: 0, n: word.len() });
1033 offset = word.len();
1034 }
1035 while offset < word.len() {
1036 let n = (word.len() - offset).min(unicode::len_utf8(word[offset]));
1037 let idx = syms.len() as isize;
1038 offset += n;
1039 syms.push(Sym { prev: idx - 1, next: if offset == word.len() { -1 } else { idx + 1 }, start: offset - n, n });
1040 }
1041 let mut queue: BinaryHeap<Bigram> = BinaryHeap::new();
1042 let add = |syms: &[Sym], queue: &mut BinaryHeap<Bigram>, left: isize, right: isize| {
1043 if left < 0 || right < 0 {
1044 return;
1045 }
1046 let (l, r) = (syms[left as usize], syms[right as usize]);
1047 let (lt, rt) = (&word[l.start..l.start + l.n], &word[r.start..r.start + r.n]);
1048 if let Some(rank) = self.bpe_rank(lt, rt) {
1049 let mut text = lt.to_vec();
1050 text.extend_from_slice(rt);
1051 queue.push(Bigram { rank, left: left as usize, right: right as usize, text });
1052 }
1053 };
1054 for i in 1..syms.len() {
1055 add(&syms, &mut queue, i as isize - 1, i as isize);
1056 }
1057 while let Some(b) = queue.pop() {
1058 let (l, r) = (syms[b.left], syms[b.right]);
1059 if l.n == 0 || r.n == 0 {
1060 continue;
1061 }
1062 let joined_now = &word[l.start..l.start + l.n].iter().chain(&word[r.start..r.start + r.n]).copied().collect::<Vec<u8>>();
1063 if *joined_now != b.text {
1064 continue;
1065 }
1066 syms[b.left].n += r.n;
1067 syms[b.right].n = 0;
1068 syms[b.left].next = r.next;
1069 if r.next >= 0 {
1070 syms[r.next as usize].prev = b.left as isize;
1071 }
1072 let prev = syms[b.left].prev;
1073 let next = syms[b.left].next;
1074 add(&syms, &mut queue, prev, b.left as isize);
1075 add(&syms, &mut queue, b.left as isize, next);
1076 }
1077 syms.iter().filter(|s| s.n > 0).map(|s| &word[s.start..s.start + s.n]).collect()
1078 }
1079
1080 fn bpe_dna(&self, text: &[u8], out: &mut Vec<u32>) -> Result<(), TokenizeError> {
1082 let (Some(open), Some(close), Some(oov)) =
1083 (self.text_to_token(b"<dna>"), self.text_to_token(b"</dna>"), self.text_to_token(b"<oov>"))
1084 else {
1085 return self.bpe(text, out);
1086 };
1087 const K: usize = 6;
1088 let mut pos = 0;
1089 while pos < text.len() {
1090 let Some(start) = find(text, b"<dna>", pos) else {
1091 self.bpe(&text[pos..], out)?;
1092 break;
1093 };
1094 if start > pos {
1095 self.bpe(&text[pos..start], out)?;
1096 }
1097 out.push(open);
1098 let content = start + 5;
1099 let end = find(text, b"</dna>", content);
1100 let seq: Vec<u8> = text[content..end.unwrap_or(text.len())].iter().map(|c| c.to_ascii_uppercase()).collect();
1101 let kmer = |k: &[u8]| {
1102 let mut t = k.to_vec();
1103 t.extend_from_slice(DNA_KMER_MARKER);
1104 self.text_to_token(&t).unwrap_or(oov)
1105 };
1106 let mut i = 0;
1107 while i + K <= seq.len() {
1108 out.push(kmer(&seq[i..i + K]));
1109 i += K;
1110 }
1111 if i < seq.len() {
1112 let mut tail = seq[i..].to_vec();
1113 tail.resize(K, b'A');
1114 out.push(kmer(&tail));
1115 }
1116 let Some(end) = end else { break };
1117 out.push(close);
1118 pos = end + 6;
1119 }
1120 Ok(())
1121 }
1122
1123 fn bpe_whitespace(&self, text: &[u8], out: &mut Vec<u32>) -> Result<(), TokenizeError> {
1125 let mut seg = Vec::new();
1126 for c in unicode::cpts_from_utf8(text) {
1127 if unicode::flags(c).is_whitespace() {
1128 if !seg.is_empty() {
1129 self.bpe(&seg, out)?;
1130 seg.clear();
1131 }
1132 } else {
1133 unicode::push_utf8(if self.lowercase { unicode::to_lower(c) } else { c }, &mut seg)?;
1134 }
1135 }
1136 if !seg.is_empty() {
1137 self.bpe(&seg, out)?;
1138 }
1139 Ok(())
1140 }
1141
1142 fn piece_uncached(&self, id: u32) -> Vec<u8> {
1144 let t = &self.tokens[id as usize];
1145 let a = t.attr;
1146 if a & (attr::UNKNOWN | attr::CONTROL | attr::USER_DEFINED) != 0 {
1147 return t.text.clone();
1148 }
1149 if a & attr::NORMAL != 0 {
1150 return match self.kind {
1151 VocabKind::Spm => replace_all(&t.text, ESCAPED_SPACE, b" "),
1152 VocabKind::Bpe(_) if self.escape_whitespaces => replace_all(&t.text, ESCAPED_SPACE, b" "),
1153 VocabKind::Bpe(_) => decode_text(&t.text),
1154 };
1155 }
1156 if a & attr::BYTE != 0 {
1157 return vec![self.token_to_byte(id)];
1158 }
1159 Vec::new()
1160 }
1161
1162 pub fn token_to_piece(&self, id: u32, lstrip: usize, special: bool) -> &[u8] {
1165 if !special && self.tokens[id as usize].attr & (attr::UNKNOWN | attr::CONTROL) != 0 {
1166 return &[];
1167 }
1168 let p = &self.piece_cache[id as usize];
1169 let strip = p.iter().take(lstrip).take_while(|&&c| c == b' ').count();
1170 &p[strip..]
1171 }
1172
1173 pub fn detokenize(&self, tokens: &[u32], remove_special: bool, unparse_special: bool) -> Vec<u8> {
1176 let mut toks = tokens;
1177 let mut remove_space = self.add_space_prefix;
1178 if remove_special && self.add_bos && !toks.is_empty() && Some(toks[0]) == self.specials.bos {
1179 remove_space = false;
1180 toks = &toks[1..];
1181 }
1182 if remove_special && self.add_eos && !toks.is_empty() && Some(toks[toks.len() - 1]) == self.specials.eos {
1183 toks = &toks[..toks.len() - 1];
1184 }
1185 let mut text = Vec::new();
1186 for &t in toks {
1187 text.extend_from_slice(self.token_to_piece(t, remove_space as usize, unparse_special));
1188 remove_space = false;
1189 }
1190 if self.clean_spaces {
1191 clean_spaces(&mut text);
1192 }
1193 text
1194 }
1195}
1196
1197fn decode_text(text: &[u8]) -> Vec<u8> {
1200 let mut out = Vec::with_capacity(text.len());
1201 for c in unicode::cpts_from_utf8(text) {
1202 match unicode::utf8_to_byte(c) {
1203 Some(b) => out.push(b),
1204 None => {
1205 let mut u = Vec::new();
1206 if unicode::push_utf8(c, &mut u).is_err() {
1207 continue;
1208 }
1209 out.extend_from_slice(b"[UNK_BYTE_0x");
1210 for b in &u {
1211 out.extend_from_slice(format!("{b:02x}").as_bytes());
1212 }
1213 out.extend_from_slice(text);
1214 out.push(b']');
1215 }
1216 }
1217 }
1218 out
1219}
1220
1221fn clean_spaces(text: &mut Vec<u8>) {
1224 let mut total = text.len();
1225 let total1 = total;
1226 total = if total > 0 { 1 } else { 0 };
1227 for i in 1..total1 {
1228 let x = text[i];
1229 if text[i - 1] == b' ' && matches!(x, b'?' | b'!' | b'.' | b',') {
1230 total -= 1;
1231 }
1232 text[total] = x;
1233 total += 1;
1234 }
1235 let total2 = total;
1236 total = if total > 0 { 1 } else { 0 };
1237 let mut i = 1;
1238 while i < total2 {
1239 let x = text[i];
1240 if x == b'\'' && i + 1 < total2 && text[i - 1] == b' ' && text[i + 1] == b' ' {
1241 total -= 1;
1242 i += 1;
1243 text[i] = 0;
1244 }
1245 text[total] = x;
1246 total += 1;
1247 i += 1;
1248 }
1249 let total3 = total;
1250 total = if total > 0 { 1 } else { 0 };
1251 for i in 1..total3 {
1252 let x = text[i];
1253 if text[i - 1] == b' ' && x == b'\'' && i + 1 < total3 {
1254 let x1 = text[i + 1];
1255 if x1 == b't' || x1 == b'd' {
1256 } else if x1 == b's' || x1 == b'm' {
1257 total -= 1;
1258 } else if i + 2 < total3 {
1259 let x2 = text[i + 2];
1260 if (x1 == b'r' && x2 == b'e') || (x1 == b'v' && x2 == b'e') {
1261 total -= 1;
1262 }
1263 }
1264 }
1265 text[total] = x;
1266 total += 1;
1267 }
1268 text.truncate(total);
1269}