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 fn validate_prefix_cache(&self) -> Result<()> {
62 Ok(())
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::HuggingFaceTokenizer;
70
71 const TOKENIZER_PATH: &str = concat!(
74 env!("CARGO_MANIFEST_DIR"),
75 "/tests/data/minimal-bpe/tokenizer.json"
76 );
77
78 #[test]
79 fn test_fast_encode_decode_roundtrip() {
80 let tokenizer = FastTokenizer::from_file(TOKENIZER_PATH).unwrap();
81 let text = "Hello, world!";
86 let encoding = tokenizer.encode(text).unwrap();
87 assert!(!encoding.token_ids().is_empty());
88 let decoded: String = tokenizer.decode(encoding.token_ids(), true).unwrap().into();
89 assert!(!decoded.is_empty());
90 let enc_chars: String = text.chars().filter(|c| !c.is_whitespace()).collect();
92 let dec_chars: String = decoded.chars().filter(|c| !c.is_whitespace()).collect();
93 assert_eq!(
94 enc_chars, dec_chars,
95 "non-space characters must be preserved"
96 );
97 }
98
99 #[test]
100 fn test_fast_matches_hf_encoding() {
101 let fast = FastTokenizer::from_file(TOKENIZER_PATH).unwrap();
102 let hf = HuggingFaceTokenizer::from_file(TOKENIZER_PATH).unwrap();
103
104 for text in &["Hello, world!", "Hello", " world", "He llo"] {
105 let fast_ids = fast.encode(text).unwrap();
106 let hf_ids = hf.encode(text).unwrap();
107 assert_eq!(
108 fast_ids.token_ids(),
109 hf_ids.token_ids(),
110 "fastokens and HuggingFace must produce identical token IDs for '{text}'"
111 );
112 }
113 }
114
115 #[test]
116 fn test_fast_batch_encode() {
117 let tokenizer = FastTokenizer::from_file(TOKENIZER_PATH).unwrap();
118 let inputs = &["Hello", " world", "Hello, world!"];
119 let encodings = tokenizer.encode_batch(inputs).unwrap();
120 assert_eq!(encodings.len(), inputs.len());
121 for (enc, input) in encodings.iter().zip(inputs.iter()) {
122 assert!(
123 !enc.token_ids().is_empty(),
124 "encoding for '{input}' must be non-empty"
125 );
126 }
127 }
128
129 #[test]
130 fn test_fast_with_decode_stream() {
131 use crate::Tokenizer as TokenizerWrapper;
132 use std::sync::Arc;
133
134 let tokenizer = Arc::new(FastTokenizer::from_file(TOKENIZER_PATH).unwrap());
135 let wrapper = TokenizerWrapper::from(tokenizer);
136
137 let prompt_ids = wrapper.encode("Hello").unwrap().token_ids().to_vec();
139 let continuation = ", world!";
140 let cont_ids = wrapper.encode(continuation).unwrap().token_ids().to_vec();
141
142 let mut stream = wrapper.decode_stream(&prompt_ids, true);
143 let mut accumulated = String::new();
145 for id in &cont_ids {
146 if let Some(chunk) = stream.step(*id).unwrap() {
147 accumulated.push_str(&chunk);
148 }
149 }
150
151 let mut all_ids = prompt_ids.clone();
155 all_ids.extend_from_slice(&cont_ids);
156 let full_text: String = wrapper.decode(&all_ids, true).unwrap().into();
157 let prompt_text: String = wrapper.decode(&prompt_ids, true).unwrap().into();
158 let expected = &full_text[prompt_text.len()..];
159 assert_eq!(
160 accumulated, expected,
161 "streamed chunks must equal context-aware decoded continuation"
162 );
163 }
164}