dynamo_tokenizers/
fastokens.rs1use std::path::Path;
11
12use rayon::prelude::*;
13
14use super::{
15 Encoding, Error, Result, TokenIdType,
16 hf::HuggingFaceTokenizer,
17 traits::{DecodeResult, Decoder, Encoder, Tokenizer},
18};
19
20pub struct FastTokenizer {
24 fast_encoder: fastokens::Tokenizer,
25 hf_decoder: HuggingFaceTokenizer,
26}
27
28impl FastTokenizer {
29 pub fn from_file(path: &str) -> Result<Self> {
30 let fast_encoder = fastokens::Tokenizer::from_file(Path::new(path))
31 .map_err(|e| Error::msg(format!("Error loading fastokens tokenizer: {e}")))?;
32 let hf_decoder = HuggingFaceTokenizer::from_file(path)?;
33 Ok(Self {
34 fast_encoder,
35 hf_decoder,
36 })
37 }
38}
39
40impl Encoder for FastTokenizer {
41 fn encode(&self, input: &str) -> Result<Encoding> {
42 let ids = self
43 .fast_encoder
44 .encode(input)
45 .map_err(|e| Error::msg(format!("Fastokens encode error: {e}")))?;
46 Ok(Encoding::Sp(ids))
47 }
48
49 fn encode_batch(&self, inputs: &[&str]) -> Result<Vec<Encoding>> {
50 inputs.par_iter().map(|input| self.encode(input)).collect()
51 }
52}
53
54impl Decoder for FastTokenizer {
55 fn decode(&self, token_ids: &[TokenIdType], skip_special_tokens: bool) -> Result<DecodeResult> {
56 self.hf_decoder.decode(token_ids, skip_special_tokens)
57 }
58}
59
60impl Tokenizer for FastTokenizer {}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65 use crate::HuggingFaceTokenizer;
66
67 const TOKENIZER_PATH: &str = concat!(
70 env!("CARGO_MANIFEST_DIR"),
71 "/../llm/tests/data/sample-models/minimal-bpe/tokenizer.json"
72 );
73
74 #[test]
75 fn test_fast_encode_decode_roundtrip() {
76 let tokenizer = FastTokenizer::from_file(TOKENIZER_PATH).unwrap();
77 let text = "Hello, world!";
82 let encoding = tokenizer.encode(text).unwrap();
83 assert!(!encoding.token_ids().is_empty());
84 let decoded: String = tokenizer.decode(encoding.token_ids(), true).unwrap().into();
85 assert!(!decoded.is_empty());
86 let enc_chars: String = text.chars().filter(|c| !c.is_whitespace()).collect();
88 let dec_chars: String = decoded.chars().filter(|c| !c.is_whitespace()).collect();
89 assert_eq!(
90 enc_chars, dec_chars,
91 "non-space characters must be preserved"
92 );
93 }
94
95 #[test]
96 fn test_fast_matches_hf_encoding() {
97 let fast = FastTokenizer::from_file(TOKENIZER_PATH).unwrap();
98 let hf = HuggingFaceTokenizer::from_file(TOKENIZER_PATH).unwrap();
99
100 for text in &["Hello, world!", "Hello", " world", "He llo"] {
101 let fast_ids = fast.encode(text).unwrap();
102 let hf_ids = hf.encode(text).unwrap();
103 assert_eq!(
104 fast_ids.token_ids(),
105 hf_ids.token_ids(),
106 "fastokens and HuggingFace must produce identical token IDs for '{text}'"
107 );
108 }
109 }
110
111 #[test]
112 fn test_fast_batch_encode() {
113 let tokenizer = FastTokenizer::from_file(TOKENIZER_PATH).unwrap();
114 let inputs = &["Hello", " world", "Hello, world!"];
115 let encodings = tokenizer.encode_batch(inputs).unwrap();
116 assert_eq!(encodings.len(), inputs.len());
117 for (enc, input) in encodings.iter().zip(inputs.iter()) {
118 assert!(
119 !enc.token_ids().is_empty(),
120 "encoding for '{input}' must be non-empty"
121 );
122 }
123 }
124
125 #[test]
126 fn test_fast_with_decode_stream() {
127 use crate::Tokenizer as TokenizerWrapper;
128 use std::sync::Arc;
129
130 let tokenizer = Arc::new(FastTokenizer::from_file(TOKENIZER_PATH).unwrap());
131 let wrapper = TokenizerWrapper::from(tokenizer);
132
133 let prompt_ids = wrapper.encode("Hello").unwrap().token_ids().to_vec();
135 let continuation = ", world!";
136 let cont_ids = wrapper.encode(continuation).unwrap().token_ids().to_vec();
137
138 let mut stream = wrapper.decode_stream(&prompt_ids, true);
139 let mut accumulated = String::new();
141 for id in &cont_ids {
142 if let Some(chunk) = stream.step(*id).unwrap() {
143 accumulated.push_str(&chunk);
144 }
145 }
146
147 let mut all_ids = prompt_ids.clone();
151 all_ids.extend_from_slice(&cont_ids);
152 let full_text: String = wrapper.decode(&all_ids, true).unwrap().into();
153 let prompt_text: String = wrapper.decode(&prompt_ids, true).unwrap().into();
154 let expected = &full_text[prompt_text.len()..];
155 assert_eq!(
156 accumulated, expected,
157 "streamed chunks must equal context-aware decoded continuation"
158 );
159 }
160}