1use std::collections::HashMap;
4use std::sync::RwLock;
5
6use rustc_hash::FxHashMap;
7use serde::de::Error as _;
8use serde::ser::{Error as _, SerializeStruct};
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10
11use super::lattice::Lattice;
12use super::prng::Prng;
13use super::trie::Trie;
14use crate::Token;
15use crate::error::{Error, Result};
16use crate::traits::Model;
17
18pub(crate) const UNK_PENALTY: f64 = 10.0;
20
21const CACHE_MAX_LENGTH: usize = 256;
23const CACHE_CAPACITY: usize = 10_000;
25const CACHE_SHARDS: usize = 64;
27
28#[derive(Debug)]
34struct SentenceCache {
35 shards: Vec<RwLock<FxHashMap<String, Vec<String>>>>,
36}
37
38impl SentenceCache {
39 fn new() -> Self {
40 Self {
41 shards: (0..CACHE_SHARDS)
42 .map(|_| RwLock::new(FxHashMap::default()))
43 .collect(),
44 }
45 }
46
47 fn shard(&self, key: &str) -> &RwLock<FxHashMap<String, Vec<String>>> {
48 use std::hash::{BuildHasher, BuildHasherDefault};
49 let h = BuildHasherDefault::<rustc_hash::FxHasher>::default().hash_one(key);
50 &self.shards[(h as usize) % self.shards.len()]
51 }
52
53 fn get(&self, key: &str) -> Option<Vec<String>> {
54 self.shard(key).try_read().ok()?.get(key).cloned()
55 }
56
57 fn insert(&self, key: &str, pieces: &[String]) {
58 if key.len() >= CACHE_MAX_LENGTH {
59 return;
60 }
61 let per_shard = CACHE_CAPACITY.div_ceil(self.shards.len());
62 if let Ok(mut map) = self.shard(key).try_write() {
63 if map.len() < per_shard {
64 map.insert(key.to_owned(), pieces.to_vec());
65 }
66 }
67 }
68
69 fn clear(&self) {
70 for shard in &self.shards {
71 if let Ok(mut map) = shard.write() {
72 map.clear();
73 }
74 }
75 }
76}
77
78pub struct Unigram {
108 vocab: Vec<(String, f64)>,
109 token_to_ids: FxHashMap<String, u32>,
110 trie: Trie,
111 pub(crate) min_score: f64,
113 unk_id: Option<usize>,
114 pub(crate) bos_id: usize,
115 pub(crate) eos_id: usize,
116 fuse_unk: bool,
117 is_optimized: bool,
118 byte_fallback: bool,
119 sampling: Option<(f64, i32)>,
121 seed: Option<u64>,
123 cache: SentenceCache,
124}
125
126impl Clone for Unigram {
127 fn clone(&self) -> Self {
129 Self {
130 vocab: self.vocab.clone(),
131 token_to_ids: self.token_to_ids.clone(),
132 trie: self.trie.clone(),
133 min_score: self.min_score,
134 unk_id: self.unk_id,
135 bos_id: self.bos_id,
136 eos_id: self.eos_id,
137 fuse_unk: self.fuse_unk,
138 is_optimized: self.is_optimized,
139 byte_fallback: self.byte_fallback,
140 sampling: self.sampling,
141 seed: self.seed,
142 cache: SentenceCache::new(),
143 }
144 }
145}
146
147impl PartialEq for Unigram {
148 fn eq(&self, other: &Self) -> bool {
149 self.unk_id == other.unk_id
150 && self.byte_fallback == other.byte_fallback
151 && self.vocab.len() == other.vocab.len()
152 && self
153 .vocab
154 .iter()
155 .zip(&other.vocab)
156 .all(|((a, sa), (b, sb))| a == b && (sa == sb || (sa.is_nan() && sb.is_nan())))
157 }
158}
159
160impl std::fmt::Debug for Unigram {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 f.debug_struct("Unigram")
163 .field("vocab", &self.vocab.len())
164 .field("unk_id", &self.unk_id)
165 .field("byte_fallback", &self.byte_fallback)
166 .finish()
167 }
168}
169
170impl Default for Unigram {
171 fn default() -> Self {
172 Self::new(vec![("<unk>".to_string(), 0.0)], Some(0), false)
173 .expect("the default vocabulary is valid")
174 }
175}
176
177impl Unigram {
178 pub fn new(
188 vocab: Vec<(String, f64)>,
189 unk_id: Option<usize>,
190 byte_fallback: bool,
191 ) -> Result<Self> {
192 if let Some((id, (piece, score))) =
193 vocab.iter().enumerate().find(|(_, (_, s))| !s.is_finite())
194 {
195 return Err(Error::Config(non_finite_score(piece, id, *score)));
196 }
197 Self::with_any_scores(vocab, unk_id, byte_fallback)
198 }
199
200 pub(crate) fn with_any_scores(
205 vocab: Vec<(String, f64)>,
206 unk_id: Option<usize>,
207 byte_fallback: bool,
208 ) -> Result<Self> {
209 if let Some(unk_id) = unk_id {
210 if vocab.is_empty() {
211 return Err(Error::Config(
212 "Unigram: the vocabulary is empty but at least <unk> is needed".into(),
213 ));
214 }
215 if unk_id >= vocab.len() {
216 return Err(Error::Config(format!(
217 "Unigram: unk_id {unk_id} is not in the vocabulary (size {})",
218 vocab.len()
219 )));
220 }
221 }
222 let n = vocab.len();
223 let mut token_to_ids = FxHashMap::default();
224 token_to_ids.reserve(n);
225 let mut trie = Trie::new();
226 let mut min_score = f64::INFINITY;
227 for (id, (token, score)) in vocab.iter().enumerate() {
228 let id = u32::try_from(id)
229 .map_err(|_| Error::Config("Unigram: vocabulary is too large".into()))?;
230 token_to_ids.insert(token.clone(), id);
231 trie.insert(token.as_bytes(), id);
232 if *score < min_score {
233 min_score = *score;
234 }
235 }
236 Ok(Self {
237 vocab,
238 token_to_ids,
239 trie,
240 min_score,
241 unk_id,
242 bos_id: n + 1,
243 eos_id: n + 2,
244 fuse_unk: true,
245 is_optimized: true,
246 byte_fallback,
247 sampling: None,
248 seed: None,
249 cache: SentenceCache::new(),
250 })
251 }
252
253 pub fn set_sampling(&mut self, alpha: f64, nbest_size: i32) -> Result<()> {
269 if !alpha.is_finite() || alpha < 0.0 {
270 return Err(Error::Config(format!(
271 "Unigram: sampling alpha must be finite and >= 0, got {alpha}"
272 )));
273 }
274 self.sampling = if alpha == 0.0 || (0..=1).contains(&nbest_size) {
275 None
276 } else {
277 Some((alpha, nbest_size))
278 };
279 Ok(())
280 }
281
282 pub fn with_sampling(mut self, alpha: f64, nbest_size: i32) -> Result<Self> {
284 self.set_sampling(alpha, nbest_size)?;
285 Ok(self)
286 }
287
288 pub fn sampling(&self) -> Option<(f64, i32)> {
291 self.sampling
292 }
293
294 pub fn set_seed(&mut self, seed: Option<u64>) {
298 self.seed = seed;
299 }
300
301 pub fn with_seed(mut self, seed: u64) -> Self {
303 self.seed = Some(seed);
304 self
305 }
306
307 pub fn seed(&self) -> Option<u64> {
309 self.seed
310 }
311
312 pub fn byte_fallback(&self) -> bool {
314 self.byte_fallback
315 }
316
317 pub fn unk_id(&self) -> Option<usize> {
319 self.unk_id
320 }
321
322 pub fn pieces(&self) -> &[(String, f64)] {
324 &self.vocab
325 }
326
327 pub(crate) fn len(&self) -> usize {
329 self.vocab.len()
330 }
331
332 pub fn clear_cache(&self) {
334 self.cache.clear();
335 }
336
337 #[cfg(test)]
338 pub(crate) fn set_fuse_unk(&mut self, fuse_unk: bool) {
339 self.fuse_unk = fuse_unk;
340 self.clear_cache();
341 }
342
343 #[cfg(test)]
344 pub(crate) fn set_optimized(&mut self, is_optimized: bool) {
345 self.is_optimized = is_optimized;
346 self.clear_cache();
347 }
348
349 pub(crate) fn populate_nodes(&self, lattice: &mut Lattice<'_>) {
353 let unk_score = self.min_score - UNK_PENALTY;
354 let sentence = lattice.sentence();
355 let bytes = sentence.as_bytes();
356 let len = sentence.len();
357 let mut pos = 0;
358 let mut found: Vec<(usize, u32)> = Vec::new();
359 while pos < len {
360 let char_len = utf8_len(bytes[pos]);
361 found.clear();
362 self.trie
363 .common_prefix_search(&bytes[pos..], |n, id| found.push((n, id)));
364 let mut has_single = false;
365 for &(n, id) in &found {
366 lattice.insert(pos, n, self.vocab[id as usize].1, id as usize);
367 has_single |= n == char_len;
368 }
369 if !has_single {
370 if let Some(unk) = self.unk_id {
371 lattice.insert(pos, char_len, unk_score, unk);
372 }
373 }
374 pos += char_len;
375 }
376 }
377
378 pub fn encode(&self, sentence: &str) -> Result<Vec<String>> {
381 if sentence.is_empty() {
382 return Ok(vec![]);
383 }
384 if let Some((alpha, nbest_size)) = self.sampling {
385 return self.encode_sampled(sentence, alpha, nbest_size);
387 }
388 if let Some(hit) = self.cache.get(sentence) {
389 return Ok(hit);
390 }
391 let result = if self.is_optimized {
392 self.encode_optimized(sentence)?
393 } else {
394 self.encode_unoptimized(sentence)?
395 };
396 self.cache.insert(sentence, &result);
397 Ok(result)
398 }
399
400 fn encode_optimized(&self, sentence: &str) -> Result<Vec<String>> {
403 #[derive(Clone, Copy)]
404 struct Best {
405 id: usize,
406 score: f64,
407 starts_at: Option<usize>,
408 }
409 let bytes = sentence.as_bytes();
410 let size = bytes.len();
411 let unk_score = self.min_score - UNK_PENALTY;
412 let mut best = vec![
413 Best {
414 id: 0,
415 score: 0.0,
416 starts_at: None,
417 };
418 size + 1
419 ];
420 let mut start = 0;
421 while start < size {
422 let base = best[start].score;
423 let char_len = utf8_len(bytes[start]);
424 let mut has_single = false;
425 let vocab = &self.vocab;
426 self.trie.common_prefix_search(&bytes[start..], |n, id| {
427 let target = &mut best[start + n];
428 let candidate = vocab[id as usize].1 + base;
429 if target.starts_at.is_none() || candidate > target.score {
430 *target = Best {
431 id: id as usize,
432 score: candidate,
433 starts_at: Some(start),
434 };
435 }
436 has_single |= n == char_len;
437 });
438 if !has_single {
439 let target = &mut best[start + char_len];
440 let candidate = unk_score + base;
441 if target.starts_at.is_none() || candidate > target.score {
442 *target = Best {
443 id: self.unk_id.ok_or_else(missing_unk)?,
444 score: candidate,
445 starts_at: Some(start),
446 };
447 }
448 }
449 start += char_len;
450 }
451
452 let mut results: Vec<String> = Vec::new();
453 let mut unk_run: Vec<&str> = Vec::new();
454 let mut end = size;
455 while end > 0 {
456 let node = best[end];
457 let start = node
458 .starts_at
459 .ok_or_else(|| Error::Model("Unigram: no segmentation found".into()))?;
460 let piece = &sentence[start..end];
461 if self.fuse_unk && Some(node.id) == self.unk_id {
462 unk_run.push(piece);
463 } else {
464 if !unk_run.is_empty() {
465 unk_run.reverse();
466 results.push(unk_run.concat());
467 unk_run.clear();
468 }
469 results.push(piece.to_owned());
470 }
471 end = start;
472 }
473 if !unk_run.is_empty() {
474 unk_run.reverse();
475 results.push(unk_run.concat());
476 }
477 results.reverse();
478 Ok(results)
479 }
480
481 fn encode_unoptimized(&self, sentence: &str) -> Result<Vec<String>> {
483 let mut lattice = Lattice::new(sentence, self.bos_id, self.eos_id);
484 self.populate_nodes(&mut lattice);
485 let path = lattice.viterbi();
486 self.pieces_for_path(&lattice, &path)
487 }
488
489 fn encode_sampled(&self, sentence: &str, alpha: f64, nbest_size: i32) -> Result<Vec<String>> {
491 let mut rng = match self.seed {
492 Some(seed) => {
493 let mut h = rustc_hash::FxHasher::default();
494 std::hash::Hasher::write(&mut h, sentence.as_bytes());
495 Prng::seeded(seed, std::hash::Hasher::finish(&h))
496 }
497 None => Prng::from_entropy(),
498 };
499 let mut lattice = Lattice::new(sentence, self.bos_id, self.eos_id);
500 self.populate_nodes(&mut lattice);
501 let path = if nbest_size < 0 {
502 let mut uniform = || rng.next_f64();
503 let sampled = lattice.sample(alpha, &mut uniform);
504 if sampled.is_empty() {
505 lattice.viterbi()
510 } else {
511 sampled
512 }
513 } else {
514 let mut paths = lattice.nbest(nbest_size as usize);
515 if paths.is_empty() {
516 return Err(missing_unk());
517 }
518 let scores: Vec<f64> = paths
519 .iter()
520 .map(|p| p.iter().map(|&i| lattice.node(i).score).sum::<f64>())
521 .collect();
522 let weights = nbest_weights(&scores, alpha);
523 paths.swap_remove(rng.choose_weighted(&weights))
524 };
525 self.pieces_for_path(&lattice, &path)
526 }
527
528 fn pieces_for_path(&self, lattice: &Lattice<'_>, path: &[usize]) -> Result<Vec<String>> {
530 if path.is_empty() {
531 return Err(missing_unk());
532 }
533 let mut results = Vec::new();
534 let mut unk_run = String::new();
535 for &i in path {
536 let node = lattice.node(i);
537 let piece = lattice.piece(node);
538 if self.fuse_unk && Some(node.id) == self.unk_id {
539 unk_run.push_str(piece);
540 } else {
541 if !unk_run.is_empty() {
542 results.push(std::mem::take(&mut unk_run));
543 }
544 results.push(piece.to_owned());
545 }
546 }
547 if !unk_run.is_empty() {
548 results.push(unk_run);
549 }
550 Ok(results)
551 }
552}
553
554fn nbest_weights(scores: &[f64], alpha: f64) -> Vec<f64> {
559 const MIN_EXPONENT: f64 = -750.0;
561 let max = scores.iter().copied().fold(f64::NEG_INFINITY, f64::max);
562 scores
563 .iter()
564 .map(|&s| {
565 let gap = s - max;
566 if gap >= 0.0 || !gap.is_finite() {
567 return if gap >= 0.0 { 1.0 } else { 0.0 };
570 }
571 let exponent = alpha * gap;
572 if exponent.is_nan() {
573 return 0.0;
574 }
575 exponent.max(MIN_EXPONENT).exp()
576 })
577 .collect()
578}
579
580fn non_finite_score(piece: &str, id: usize, score: f64) -> String {
581 format!(
582 "Unigram: piece {piece:?} (id {id}) has a non-finite score ({score}); \
583 scores must be finite to be saved in tokenizer.json"
584 )
585}
586
587fn missing_unk() -> Error {
588 Error::Model("Unigram: encountered an unknown token but `unk_id` is missing".into())
589}
590
591#[inline]
593fn utf8_len(first: u8) -> usize {
594 match first {
595 0x00..=0x7F => 1,
596 0xC0..=0xDF => 2,
597 0xE0..=0xEF => 3,
598 _ => 4,
599 }
600}
601
602impl Model for Unigram {
603 fn tokenize(&self, sequence: &str) -> Result<Vec<Token>> {
604 let pieces = self.encode(sequence)?;
605 let mut offset = 0;
606 let mut tokens = Vec::with_capacity(pieces.len());
607 for piece in pieces {
608 let len = piece.len();
609 let offsets = (offset, offset + len);
610 offset += len;
611 if let Some(&id) = self.token_to_ids.get(&piece) {
612 tokens.push(Token::new(id, piece, offsets));
613 continue;
614 }
615 if self.byte_fallback {
616 let bytes: Option<Vec<Token>> = piece
617 .bytes()
618 .map(|b| {
619 let s = format!("<0x{b:02X}>");
620 self.token_to_ids
621 .get(&s)
622 .map(|&id| Token::new(id, s, offsets))
623 })
624 .collect();
625 if let Some(bytes) = bytes {
626 tokens.extend(bytes);
627 continue;
628 }
629 }
630 let unk = self.unk_id.ok_or_else(missing_unk)? as u32;
631 tokens.push(Token::new(unk, piece, offsets));
632 }
633 Ok(tokens)
634 }
635
636 fn token_to_id(&self, token: &str) -> Option<u32> {
637 self.token_to_ids.get(token).copied()
638 }
639
640 fn id_to_token(&self, id: u32) -> Option<String> {
641 self.vocab.get(id as usize).map(|(t, _)| t.clone())
642 }
643
644 fn vocab(&self) -> HashMap<String, u32> {
645 self.token_to_ids
646 .iter()
647 .map(|(k, v)| (k.clone(), *v))
648 .collect()
649 }
650
651 fn vocab_size(&self) -> usize {
652 self.vocab.len()
653 }
654}
655
656impl Serialize for Unigram {
657 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
658 if let Some((id, (piece, score))) = self
661 .vocab
662 .iter()
663 .enumerate()
664 .find(|(_, (_, s))| !s.is_finite())
665 {
666 return Err(S::Error::custom(non_finite_score(piece, id, *score)));
667 }
668 let mut s = serializer.serialize_struct("Unigram", 4)?;
669 s.serialize_field("type", "Unigram")?;
670 s.serialize_field("unk_id", &self.unk_id)?;
671 s.serialize_field("vocab", &self.vocab)?;
672 s.serialize_field("byte_fallback", &self.byte_fallback)?;
673 s.end()
674 }
675}
676
677impl<'de> Deserialize<'de> for Unigram {
678 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
679 #[derive(Deserialize)]
680 struct Repr {
681 #[serde(rename = "type", default)]
682 ty: Option<String>,
683 vocab: Vec<(String, f64)>,
684 #[serde(default)]
685 unk_id: Option<usize>,
686 #[serde(default)]
687 byte_fallback: bool,
688 }
689 let r = Repr::deserialize(deserializer)?;
690 if let Some(ty) = &r.ty {
691 if ty != "Unigram" {
692 return Err(D::Error::custom(format!(
693 "expected model type \"Unigram\", got {ty:?}"
694 )));
695 }
696 }
697 Unigram::new(r.vocab, r.unk_id, r.byte_fallback).map_err(D::Error::custom)
698 }
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704
705 fn pieces(v: &[(&str, f64)]) -> Vec<(String, f64)> {
706 v.iter().map(|(s, f)| (s.to_string(), *f)).collect()
707 }
708
709 #[test]
710 fn populate_nodes_unk() {
711 let model = Unigram::new(pieces(&[("<unk>", 0.0)]), Some(0), false).unwrap();
712 let mut lattice = Lattice::new("abc", model.bos_id, model.eos_id);
713 model.populate_nodes(&mut lattice);
714 for pos in 0..3 {
715 assert_eq!(lattice.begin_nodes[pos].len(), 1);
716 let node_id = lattice.begin_nodes[pos][0];
717 assert_eq!(lattice.node(node_id).id, 0);
718 assert_eq!(node_id, pos + 2);
719 }
720 }
721
722 #[test]
723 fn populate_nodes_pieces() {
724 let model = Unigram::new(
725 pieces(&[
726 ("<unk>", 0.0),
727 ("a", 0.1),
728 ("b", 0.2),
729 ("ab", 0.3),
730 ("bc", 0.4),
731 ]),
732 Some(0),
733 false,
734 )
735 .unwrap();
736 let mut lattice = Lattice::new("abc", model.bos_id, model.eos_id);
737 model.populate_nodes(&mut lattice);
738 let ids = |pos: usize| -> Vec<usize> {
739 lattice.begin_nodes[pos]
740 .iter()
741 .map(|&i| lattice.node(i).id)
742 .collect()
743 };
744 assert_eq!(ids(0), vec![1, 3]);
745 assert_eq!(ids(1), vec![2, 4]);
746 assert_eq!(ids(2), vec![0]);
747 }
748
749 #[test]
750 fn nbest_weights_are_finite_for_extreme_alpha() {
751 let scores = [-1000.0, -1000.5, -1200.0];
752 let w = nbest_weights(&scores, 1e308);
754 assert_eq!(w, [1.0, 0.0, 0.0]);
755 let w = nbest_weights(&scores, f64::MAX);
756 assert_eq!(w, [1.0, 0.0, 0.0]);
757 let w = nbest_weights(&scores, 2.0);
759 assert_eq!(w[0], 1.0);
760 assert!((w[1] - (-1.0f64).exp()).abs() < 1e-12);
761 assert!((w[2] - (-400.0f64).exp()).abs() < 1e-200);
762 assert_eq!(nbest_weights(&[-5.0, -5.0], 1e308), [1.0, 1.0]);
764 assert_eq!(nbest_weights(&scores, 0.0), [1.0, 1.0, 1.0]);
765 assert_eq!(nbest_weights(&[-1.0, f64::NEG_INFINITY], 1e308), [1.0, 0.0]);
767 assert!(nbest_weights(&scores, 1e308).iter().all(|w| w.is_finite()));
768 }
769
770 #[test]
776 fn sampling_agrees_with_viterbi_on_models_without_single_char_pieces() {
777 let vocab = pieces(&[("ab", -1.0), ("abab", -1.5), ("c", -2.0)]);
778 let optimized = Unigram::new(vocab.clone(), None, false).unwrap();
779 let mut unoptimized = optimized.clone();
780 unoptimized.set_optimized(false);
781 for input in ["ab", "abab", "cabc", "c"] {
782 let expected = optimized.encode(input).map_err(|e| e.to_string());
783 assert_eq!(
784 unoptimized.encode(input).map_err(|e| e.to_string()),
785 expected,
786 "{input}"
787 );
788 for nbest in [-1, 4] {
789 let sampler = Unigram::new(vocab.clone(), None, false)
790 .unwrap()
791 .with_sampling(0.5, nbest)
792 .unwrap();
793 assert_eq!(
794 sampler.encode(input).map_err(|e| e.to_string()),
795 expected,
796 "nbest_size={nbest} on {input}"
797 );
798 }
799 }
800 let vocab = pieces(&[("ab", -1.0), ("abab", -1.5), ("a", -3.0), ("b", -3.0)]);
803 let viterbi = Unigram::new(vocab.clone(), None, false).unwrap();
804 for input in ["ab", "abab", "ababa", "ba"] {
805 let expected = viterbi.encode(input).unwrap();
806 for nbest in [-1, 4] {
807 let sampler = Unigram::new(vocab.clone(), None, false)
808 .unwrap()
809 .with_sampling(1e6, nbest)
810 .unwrap();
811 for _ in 0..10 {
812 assert_eq!(
813 sampler.encode(input).unwrap(),
814 expected,
815 "nbest_size={nbest} on {input}"
816 );
817 }
818 }
819 }
820 }
821
822 #[test]
823 fn sampling_incomplete_lattice_reports_missing_unk() {
824 let vocab = pieces(&[("ab", -1.0)]);
825 for nbest in [-1, 4] {
826 let sampler = Unigram::new(vocab.clone(), None, false)
827 .unwrap()
828 .with_sampling(0.5, nbest)
829 .unwrap();
830 let err = sampler.encode("abc").unwrap_err();
831 assert!(err.to_string().contains("unk_id"), "{err}");
832 let err = sampler.encode("ba").unwrap_err();
833 assert!(err.to_string().contains("unk_id"), "{err}");
834 }
835 }
836
837 #[test]
840 fn sampling_extreme_alpha_returns_viterbi() {
841 let vocab = pieces(&[
842 ("<unk>", -1500.0),
843 ("a", -1000.0),
844 ("b", -1000.0),
845 ("ab", -1000.5),
846 ("abc", -2100.0),
847 ("c", -1000.0),
848 ]);
849 let viterbi = Unigram::new(vocab.clone(), Some(0), false).unwrap();
850 let expected = viterbi.encode("abc").unwrap();
851 assert_eq!(expected, ["ab", "c"]);
852 for nbest in [-1, 8] {
853 let sampler = Unigram::new(vocab.clone(), Some(0), false)
854 .unwrap()
855 .with_sampling(1e308, nbest)
856 .unwrap();
857 for _ in 0..20 {
858 assert_eq!(sampler.encode("abc").unwrap(), expected, "nbest={nbest}");
859 }
860 }
861 }
862
863 #[test]
864 fn encode_simple() {
865 let model = Unigram::new(
866 pieces(&[
867 ("<unk>", 0.0),
868 ("a", 0.0),
869 ("b", 0.0),
870 ("c", 0.0),
871 ("d", 0.0),
872 ("cd", 1.0),
873 ("ab", 2.0),
874 ("abc", 5.0),
875 ("abcd", 10.0),
876 ]),
877 Some(0),
878 false,
879 )
880 .unwrap();
881 assert_eq!(model.encode("abcd").unwrap(), vec!["abcd"]);
882 assert_eq!(
883 model.encode("abcdacdxx").unwrap(),
884 vec!["abcd", "a", "cd", "xx"]
885 );
886 }
887
888 #[test]
889 fn encode_optimized_and_unoptimized_agree() {
890 let mut model = Unigram::new(
891 pieces(&[
892 ("<unk>", 0.0),
893 ("ab", 0.0),
894 ("cd", -0.1),
895 ("abc", -0.2),
896 ("a", -0.3),
897 ("b", -0.4),
898 ("c", -0.5),
899 ("ABC", -0.5),
900 ("abcdabcd", 20.0),
901 ("q", 20.5),
902 ("r", 20.5),
903 ("qr", -0.5),
904 ]),
905 Some(0),
906 false,
907 )
908 .unwrap();
909 for optimized in [true, false] {
910 model.set_optimized(optimized);
911 assert_eq!(model.encode("abc").unwrap(), vec!["abc"]);
912 assert_eq!(model.encode("AB").unwrap(), vec!["AB"]);
913 model.set_fuse_unk(false);
914 assert_eq!(model.encode("AB").unwrap(), vec!["A", "B"]);
915 model.set_fuse_unk(true);
916 assert_eq!(model.encode("AB").unwrap(), vec!["AB"]);
917 assert_eq!(model.encode("abcd").unwrap(), vec!["ab", "cd"]);
918 assert_eq!(model.encode("abcc").unwrap(), vec!["abc", "c"]);
919 assert_eq!(
920 model.encode("xabcabaabcdd").unwrap(),
921 vec!["x", "abc", "ab", "a", "ab", "cd", "d"]
922 );
923 model.set_fuse_unk(false);
924 assert_eq!(
925 model.encode("xyz東京").unwrap(),
926 vec!["x", "y", "z", "東", "京"]
927 );
928 model.set_fuse_unk(true);
929 assert_eq!(model.encode("xyz東京").unwrap(), vec!["xyz東京"]);
930 assert_eq!(model.encode("ABC").unwrap(), vec!["ABC"]);
931 assert_eq!(model.encode("abABCcd").unwrap(), vec!["ab", "ABC", "cd"]);
932 assert_eq!(
933 model.encode("ababcdabcdcd").unwrap(),
934 vec!["ab", "abcdabcd", "cd"]
935 );
936 assert_eq!(model.encode("abqrcd").unwrap(), vec!["ab", "q", "r", "cd"]);
937 }
938 }
939
940 #[test]
941 fn byte_fallback() {
942 let model = Unigram::new(
943 pieces(&[("<unk>", 0.0), ("<0xC3>", -0.01), ("<0xA9>", -0.03)]),
944 Some(0),
945 true,
946 )
947 .unwrap();
948 let tokens = model.tokenize("é").unwrap();
949 assert_eq!(
950 tokens,
951 vec![
952 Token::new(1, "<0xC3>".into(), (0, 2)),
953 Token::new(2, "<0xA9>".into(), (0, 2)),
954 ]
955 );
956 let tokens = model.tokenize("?é").unwrap();
958 assert_eq!(tokens[0].id, 0);
959 }
960
961 #[test]
962 fn no_unk_id_errors_only_when_needed() {
963 let model = Unigram::new(pieces(&[("a", -0.5)]), None, false).unwrap();
964 assert_eq!(model.encode("aa").unwrap(), vec!["a", "a"]);
965 assert!(model.encode("ab").is_err());
966 }
967
968 #[test]
969 fn validation() {
970 assert!(Unigram::new(vec![], Some(0), false).is_err());
971 assert!(Unigram::new(pieces(&[("a", 0.0)]), Some(1), false).is_err());
972 assert!(Unigram::new(vec![], None, false).is_ok());
973 }
974
975 #[test]
976 fn non_finite_scores_are_rejected() {
977 for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
978 let err = Unigram::new(pieces(&[("<unk>", 0.0), ("y", bad)]), Some(0), false)
979 .unwrap_err()
980 .to_string();
981 assert!(err.contains("\"y\" (id 1)"), "{err}");
982 }
983 }
984
985 #[test]
989 fn serialization_refuses_non_finite_scores() {
990 let model = Unigram::with_any_scores(
991 pieces(&[("<unk>", 0.0), ("x", f64::INFINITY)]),
992 Some(0),
993 false,
994 )
995 .unwrap();
996 let err = serde_json::to_string(&model).unwrap_err().to_string();
997 assert!(err.contains("\"x\" (id 1)"), "{err}");
998
999 let tok = crate::Tokenizer::new(model);
1000 assert!(tok.to_json(false).is_err());
1001 assert!(tok.to_json(true).is_err());
1002 }
1003
1004 #[cfg(not(target_arch = "wasm32"))]
1007 #[test]
1008 fn save_of_non_finite_model_leaves_existing_file_intact() {
1009 let dir = tempfile::tempdir().unwrap();
1010 let path = dir.path().join("tokenizer.json");
1011 let good = crate::Tokenizer::new(Unigram::default());
1012 good.save(&path, true).unwrap();
1013 let before = std::fs::read(&path).unwrap();
1014
1015 let bad =
1016 Unigram::with_any_scores(pieces(&[("<unk>", 0.0), ("y", f64::NAN)]), Some(0), false)
1017 .unwrap();
1018 let bad = crate::Tokenizer::new(bad);
1019 assert!(bad.save(&path, true).is_err());
1020 assert!(bad.save(&path, false).is_err());
1021 assert_eq!(std::fs::read(&path).unwrap(), before);
1022 assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1);
1024 }
1025
1026 #[test]
1027 fn serialization_round_trip() {
1028 for (v, unk) in [
1029 (pieces(&[("<unk>", 0.0), ("a", -0.5)]), Some(0)),
1030 (pieces(&[("a", -0.5), ("<unk>", 0.0)]), Some(1)),
1031 (pieces(&[("a", -0.5)]), None),
1032 ] {
1033 let model = Unigram::new(v, unk, false).unwrap();
1034 let json = serde_json::to_string(&model).unwrap();
1035 let back: Unigram = serde_json::from_str(&json).unwrap();
1036 assert_eq!(model, back);
1037 }
1038 let model = Unigram::new(pieces(&[("<unk>", 0.0), ("a", -0.5)]), Some(0), false).unwrap();
1039 assert_eq!(
1040 serde_json::to_string(&model).unwrap(),
1041 r#"{"type":"Unigram","unk_id":0,"vocab":[["<unk>",0.0],["a",-0.5]],"byte_fallback":false}"#
1042 );
1043 }
1044
1045 #[test]
1046 fn deserialize_legacy_and_reject_wrong_type() {
1047 let m: Unigram = serde_json::from_str(r#"{"unk_id":0,"vocab":[["<unk>",0.0]]}"#).unwrap();
1048 assert_eq!(m.vocab_size(), 1);
1049 assert!(serde_json::from_str::<Unigram>(r#"{"type":"BPE","vocab":[["a",0.0]]}"#).is_err());
1050 assert!(serde_json::from_str::<Unigram>(r#"{"unk_id":3,"vocab":[["a",0.0]]}"#).is_err());
1051 }
1052
1053 mod fixtures {
1056 use super::*;
1057
1058 type Expected<'a> = &'a [(&'a str, &'a [(u32, &'a str, (usize, usize))])];
1059
1060 fn load(name: &str) -> Option<(Unigram, serde_json::Value)> {
1061 let path = format!("{}/tests/data/hf/{name}.json", env!("CARGO_MANIFEST_DIR"));
1062 let Ok(raw) = std::fs::read_to_string(&path) else {
1063 eprintln!("skipping: {path} not found (run scripts/fetch-hf-fixtures.sh)");
1064 return None;
1065 };
1066 let file: serde_json::Value = serde_json::from_str(&raw).unwrap();
1067 let model_json = file["model"].clone();
1068 let model: Unigram = serde_json::from_value(model_json.clone()).unwrap();
1069 Some((model, model_json))
1070 }
1071
1072 fn check(name: &str, expected: Expected<'_>) {
1073 let Some((model, mut json)) = load(name) else {
1074 return;
1075 };
1076 let obj = json.as_object_mut().unwrap();
1078 obj.entry("type").or_insert("Unigram".into());
1079 obj.entry("byte_fallback").or_insert(false.into());
1080 assert_eq!(
1081 serde_json::to_value(&model).unwrap(),
1082 json,
1083 "{name}: re-serialization"
1084 );
1085
1086 for (input, tokens) in expected {
1087 let got: Vec<(u32, String, (usize, usize))> = model
1088 .tokenize(input)
1089 .unwrap()
1090 .into_iter()
1091 .map(|t| (t.id, t.value, t.offsets))
1092 .collect();
1093 let want: Vec<(u32, String, (usize, usize))> = tokens
1094 .iter()
1095 .map(|(id, v, o)| (*id, v.to_string(), *o))
1096 .collect();
1097 assert_eq!(got, want, "{name}: {input:?}");
1098 }
1099 }
1100
1101 #[test]
1103 fn t5_small() {
1104 check(
1105 "t5-small",
1106 &[
1107 ("▁Hello", &[(8774, "▁Hello", (0, 8))]),
1108 (
1109 "▁world▁test",
1110 &[(296, "▁world", (0, 8)), (794, "▁test", (8, 15))],
1111 ),
1112 (
1113 "▁𝔘nicode",
1114 &[
1115 (3, "▁", (0, 3)),
1116 (2, "𝔘", (3, 7)),
1117 (29, "n", (7, 8)),
1118 (23, "i", (8, 9)),
1119 (4978, "code", (9, 13)),
1120 ],
1121 ),
1122 (
1123 "▁日本語のテキスト",
1124 &[(3, "▁", (0, 3)), (2, "日本語のテキスト", (3, 27))],
1125 ),
1126 (
1127 "▁Ünïcödé▁ťéxt",
1128 &[
1129 (3, "▁", (0, 3)),
1130 (14858, "Ü", (3, 5)),
1131 (29, "n", (5, 6)),
1132 (2, "ï", (6, 8)),
1133 (75, "c", (8, 9)),
1134 (1872, "ö", (9, 11)),
1135 (3764, "dé", (11, 14)),
1136 (3, "▁", (14, 17)),
1137 (2, "ť", (17, 19)),
1138 (154, "é", (19, 21)),
1139 (226, "x", (21, 22)),
1140 (17, "t", (22, 23)),
1141 ],
1142 ),
1143 (
1144 "▁supercalifragilistic",
1145 &[
1146 (1355, "▁super", (0, 8)),
1147 (15534, "cali", (8, 12)),
1148 (20791, "frag", (12, 16)),
1149 (173, "il", (16, 18)),
1150 (3040, "istic", (18, 23)),
1151 ],
1152 ),
1153 ("▁a", &[(3, "▁", (0, 3)), (9, "a", (3, 4))]),
1154 ("▁😀👍", &[(3, "▁", (0, 3)), (2, "😀👍", (3, 11))]),
1155 ],
1156 );
1157 }
1158
1159 #[test]
1160 fn xlm_roberta_base() {
1161 check(
1162 "xlm-roberta-base",
1163 &[
1164 ("▁Hello", &[(35378, "▁Hello", (0, 8))]),
1165 (
1166 "▁world▁test",
1167 &[(8999, "▁world", (0, 8)), (3034, "▁test", (8, 15))],
1168 ),
1169 (
1170 "▁𝔘nicode",
1171 &[
1172 (6, "▁", (0, 3)),
1173 (3, "𝔘", (3, 7)),
1174 (12150, "nico", (7, 11)),
1175 (112, "de", (11, 13)),
1176 ],
1177 ),
1178 (
1179 "▁日本語のテキスト",
1180 &[
1181 (6, "▁", (0, 3)),
1182 (98449, "日本語", (3, 12)),
1183 (154, "の", (12, 15)),
1184 (193508, "テキスト", (15, 27)),
1185 ],
1186 ),
1187 (
1188 "▁Ünïcödé▁ťéxt",
1189 &[
1190 (8578, "▁Ü", (0, 5)),
1191 (19, "n", (5, 6)),
1192 (9392, "ï", (6, 8)),
1193 (238, "c", (8, 9)),
1194 (21166, "öd", (9, 12)),
1195 (446, "é", (12, 14)),
1196 (6, "▁", (14, 17)),
1197 (1981, "ť", (17, 19)),
1198 (446, "é", (19, 21)),
1199 (29062, "xt", (21, 23)),
1200 ],
1201 ),
1202 (
1203 "▁supercalifragilistic",
1204 &[
1205 (1601, "▁super", (0, 8)),
1206 (52777, "cali", (8, 12)),
1207 (6000, "fra", (12, 15)),
1208 (8726, "gil", (15, 18)),
1209 (48242, "istic", (18, 23)),
1210 ],
1211 ),
1212 ("▁a", &[(10, "▁a", (0, 4))]),
1213 ("▁😀👍", &[(21119, "▁😀", (0, 7)), (118280, "👍", (7, 11))]),
1214 ],
1215 );
1216 }
1217
1218 #[test]
1219 fn albert_base_v2() {
1220 check(
1221 "albert-base-v2",
1222 &[
1223 (
1224 "▁Hello",
1225 &[(13, "▁", (0, 3)), (1, "H", (3, 4)), (7523, "ello", (4, 8))],
1226 ),
1227 (
1228 "▁world▁test",
1229 &[(126, "▁world", (0, 8)), (1289, "▁test", (8, 15))],
1230 ),
1231 (
1232 "▁𝔘nicode",
1233 &[
1234 (13, "▁", (0, 3)),
1235 (1, "𝔘", (3, 7)),
1236 (889, "ni", (7, 9)),
1237 (9375, "code", (9, 13)),
1238 ],
1239 ),
1240 (
1241 "▁日本語のテキスト",
1242 &[(13, "▁", (0, 3)), (1, "日本語のテキスト", (3, 27))],
1243 ),
1244 (
1245 "▁Ünïcödé▁ťéxt",
1246 &[
1247 (13, "▁", (0, 3)),
1248 (1, "Ü", (3, 5)),
1249 (103, "n", (5, 6)),
1250 (1, "ï", (6, 8)),
1251 (150, "c", (8, 9)),
1252 (1, "ö", (9, 11)),
1253 (43, "d", (11, 12)),
1254 (1, "é", (12, 14)),
1255 (13, "▁", (14, 17)),
1256 (1, "ťé", (17, 21)),
1257 (396, "x", (21, 22)),
1258 (38, "t", (22, 23)),
1259 ],
1260 ),
1261 (
1262 "▁supercalifragilistic",
1263 &[
1264 (1026, "▁super", (0, 8)),
1265 (3430, "cal", (8, 11)),
1266 (49, "i", (11, 12)),
1267 (22133, "frag", (12, 16)),
1268 (947, "il", (16, 18)),
1269 (3771, "istic", (18, 23)),
1270 ],
1271 ),
1272 ("▁a", &[(21, "▁a", (0, 4))]),
1273 ("▁😀👍", &[(13, "▁", (0, 3)), (1, "😀👍", (3, 11))]),
1274 ],
1275 );
1276 }
1277
1278 #[test]
1279 fn large_vocab_encoding_is_fast() {
1280 let Some((model, _)) = load("xlm-roberta-base") else {
1281 return;
1282 };
1283 let words: Vec<String> = (0..20_000).map(|i| format!("▁word{i}xyz")).collect();
1284 let start = std::time::Instant::now();
1285 for w in &words {
1286 model.tokenize(w).unwrap();
1287 }
1288 let elapsed = start.elapsed();
1289 assert!(elapsed.as_secs() < 5, "encoding took {elapsed:?}");
1292 }
1293 }
1294}