dynamo_tokenizers/
fastokens.rs1use std::path::Path;
11
12use rayon::prelude::*;
13
14use super::{
15 EncodeSegment, 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 fn encode_segments(&self, segments: &[EncodeSegment<'_>]) -> Result<Encoding> {
54 let segments: Vec<fastokens::EncodeSegment<'_>> = segments
55 .iter()
56 .map(|segment| fastokens::EncodeSegment {
57 text: segment.text,
58 allow_special: segment.allow_special,
59 })
60 .collect();
61 let ids = self
62 .fast_encoder
63 .encode_segments(&segments)
64 .map_err(|e| Error::msg(format!("Fastokens segmented encode error: {e}")))?;
65 Ok(Encoding::Sp(ids))
66 }
67}
68
69impl Decoder for FastTokenizer {
70 fn decode(&self, token_ids: &[TokenIdType], skip_special_tokens: bool) -> Result<DecodeResult> {
71 self.hf_decoder.decode(token_ids, skip_special_tokens)
72 }
73}
74
75impl Tokenizer for FastTokenizer {
76 fn validate_prefix_cache(&self) -> Result<()> {
77 Ok(())
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::HuggingFaceTokenizer;
85
86 const TOKENIZER_PATH: &str = concat!(
89 env!("CARGO_MANIFEST_DIR"),
90 "/tests/data/minimal-bpe/tokenizer.json"
91 );
92 const SEGMENTED_TOKENIZER_PATH: &str = concat!(
93 env!("CARGO_MANIFEST_DIR"),
94 "/tests/data/sample-models/TinyLlama_v1.1/tokenizer.json"
95 );
96
97 #[test]
98 fn test_fast_encode_decode_roundtrip() {
99 let tokenizer = FastTokenizer::from_file(TOKENIZER_PATH).unwrap();
100 let text = "Hello, world!";
105 let encoding = tokenizer.encode(text).unwrap();
106 assert!(!encoding.token_ids().is_empty());
107 let decoded: String = tokenizer.decode(encoding.token_ids(), true).unwrap().into();
108 assert!(!decoded.is_empty());
109 let enc_chars: String = text.chars().filter(|c| !c.is_whitespace()).collect();
111 let dec_chars: String = decoded.chars().filter(|c| !c.is_whitespace()).collect();
112 assert_eq!(
113 enc_chars, dec_chars,
114 "non-space characters must be preserved"
115 );
116 }
117
118 #[test]
119 fn test_fast_matches_hf_encoding() {
120 let fast = FastTokenizer::from_file(TOKENIZER_PATH).unwrap();
121 let hf = HuggingFaceTokenizer::from_file(TOKENIZER_PATH).unwrap();
122
123 for text in &["Hello, world!", "Hello", " world", "He llo"] {
124 let fast_ids = fast.encode(text).unwrap();
125 let hf_ids = hf.encode(text).unwrap();
126 assert_eq!(
127 fast_ids.token_ids(),
128 hf_ids.token_ids(),
129 "fastokens and HuggingFace must produce identical token IDs for '{text}'"
130 );
131 }
132 }
133
134 #[test]
135 fn test_fast_batch_encode() {
136 let tokenizer = FastTokenizer::from_file(TOKENIZER_PATH).unwrap();
137 let inputs = &["Hello", " world", "Hello, world!"];
138 let encodings = tokenizer.encode_batch(inputs).unwrap();
139 assert_eq!(encodings.len(), inputs.len());
140 for (enc, input) in encodings.iter().zip(inputs.iter()) {
141 assert!(
142 !enc.token_ids().is_empty(),
143 "encoding for '{input}' must be non-empty"
144 );
145 }
146 }
147
148 #[test]
149 fn test_fast_segmented_encoding_preserves_trust_boundaries() {
150 let tokenizer = FastTokenizer::from_file(SEGMENTED_TOKENIZER_PATH).unwrap();
151 let upstream =
152 fastokens::Tokenizer::from_file(std::path::Path::new(SEGMENTED_TOKENIZER_PATH))
153 .unwrap();
154 let marker = "<s>";
155
156 let trusted = tokenizer
157 .encode_segments(&[EncodeSegment::control(marker)])
158 .unwrap();
159 assert_eq!(
160 trusted.token_ids(),
161 &[upstream.token_to_id(marker).unwrap()],
162 "trusted renderer output must recognize the control token"
163 );
164
165 let ordinary = tokenizer
166 .encode_segments(&[EncodeSegment::ordinary(marker)])
167 .unwrap();
168 assert_ne!(
169 ordinary.token_ids(),
170 trusted.token_ids(),
171 "untrusted content must encode the control-token spelling as ordinary text"
172 );
173
174 let segments = [
175 EncodeSegment::ordinary("hello "),
176 EncodeSegment::control(marker),
177 EncodeSegment::ordinary(marker),
178 ];
179 let upstream_segments = [
180 fastokens::EncodeSegment::ordinary("hello "),
181 fastokens::EncodeSegment::special(marker),
182 fastokens::EncodeSegment::ordinary(marker),
183 ];
184 let actual = tokenizer.encode_segments(&segments).unwrap();
185 let expected = upstream.encode_segments(&upstream_segments).unwrap();
186 assert_eq!(actual.token_ids(), expected);
187
188 assert!(
189 tokenizer
190 .encode_segments(&[])
191 .unwrap()
192 .token_ids()
193 .is_empty()
194 );
195 }
196
197 #[test]
198 fn test_fast_with_decode_stream() {
199 use crate::Tokenizer as TokenizerWrapper;
200 use std::sync::Arc;
201
202 let tokenizer = Arc::new(FastTokenizer::from_file(TOKENIZER_PATH).unwrap());
203 let wrapper = TokenizerWrapper::from(tokenizer);
204
205 let prompt_ids = wrapper.encode("Hello").unwrap().token_ids().to_vec();
207 let continuation = ", world!";
208 let cont_ids = wrapper.encode(continuation).unwrap().token_ids().to_vec();
209
210 let mut stream = wrapper.decode_stream(&prompt_ids, true);
211 let mut accumulated = String::new();
213 for id in &cont_ids {
214 if let Some(chunk) = stream.step(*id).unwrap() {
215 accumulated.push_str(&chunk);
216 }
217 }
218
219 let mut all_ids = prompt_ids.clone();
223 all_ids.extend_from_slice(&cont_ids);
224 let full_text: String = wrapper.decode(&all_ids, true).unwrap().into();
225 let prompt_text: String = wrapper.decode(&prompt_ids, true).unwrap().into();
226 let expected = &full_text[prompt_text.len()..];
227 assert_eq!(
228 accumulated, expected,
229 "streamed chunks must equal context-aware decoded continuation"
230 );
231 }
232}