dynamo_bench/coding/
tokenizer.rs1use anyhow::{Context, Result, anyhow, bail};
5use std::path::{Path, PathBuf};
6use tokenizers::Tokenizer;
7
8#[derive(Clone, Debug)]
9pub struct HfTokenizerFactory {
10 tokenizer_path: PathBuf,
11}
12
13impl HfTokenizerFactory {
14 pub fn resolve(model_or_path: &str) -> Result<Self> {
15 Ok(Self {
16 tokenizer_path: resolve_tokenizer_path(model_or_path)?,
17 })
18 }
19}
20
21pub trait TokenizerWorker: Send + 'static {
22 fn encode(&mut self, text: &str) -> Result<Vec<u32>>;
23
24 fn encode_with_word_overlap(
25 &mut self,
26 text: &str,
27 previous_text: Option<&str>,
28 previous_tokens: Option<&[u32]>,
29 overlap_words: usize,
30 ) -> Result<Vec<u32>> {
31 if overlap_words == 0 {
32 return self.encode(text);
33 }
34 let Some(previous_text) = previous_text else {
35 return self.encode(text);
36 };
37 let Some(previous_tokens) = previous_tokens else {
38 return self.encode(text);
39 };
40 if !text.starts_with(previous_text) {
41 return self.encode(text);
42 }
43
44 let overlap_start = last_word_overlap_start(previous_text, overlap_words);
45 let previous_suffix_tokens = self.encode(&previous_text[overlap_start..])?;
46 let prefix_token_count = previous_tokens
47 .len()
48 .saturating_sub(previous_suffix_tokens.len());
49 let suffix_tokens = self.encode(&text[overlap_start..])?;
50
51 let mut merged = Vec::with_capacity(prefix_token_count + suffix_tokens.len());
52 merged.extend_from_slice(&previous_tokens[..prefix_token_count]);
53 merged.extend(suffix_tokens);
54 Ok(merged)
55 }
56}
57
58pub trait TokenizerFactory: Clone + Send + Sync + 'static {
59 type Worker: TokenizerWorker;
60
61 fn create_worker(&self) -> Result<Self::Worker>;
62}
63
64pub struct LazyTokenizer<F: TokenizerFactory> {
67 factory: F,
68 worker: Option<F::Worker>,
69}
70
71impl<F: TokenizerFactory> LazyTokenizer<F> {
72 pub fn new(factory: F) -> Self {
73 Self {
74 factory,
75 worker: None,
76 }
77 }
78}
79
80impl<F: TokenizerFactory> TokenizerWorker for LazyTokenizer<F> {
81 fn encode(&mut self, text: &str) -> Result<Vec<u32>> {
82 if self.worker.is_none() {
83 let worker = self
84 .factory
85 .create_worker()
86 .context("failed to initialize tokenizer")?;
87 self.worker = Some(worker);
88 }
89 self.worker
90 .as_mut()
91 .expect("tokenizer worker was created")
92 .encode(text)
93 }
94}
95
96impl TokenizerFactory for HfTokenizerFactory {
97 type Worker = HfTokenizerWorker;
98
99 fn create_worker(&self) -> Result<Self::Worker> {
100 HfTokenizerWorker::from_file(&self.tokenizer_path)
101 }
102}
103
104pub struct HfTokenizerWorker {
105 tokenizer: Tokenizer,
106}
107
108impl HfTokenizerWorker {
109 pub fn from_file(path: &Path) -> Result<Self> {
110 let tokenizer = Tokenizer::from_file(path).map_err(|error| {
111 anyhow!("failed to load tokenizer from {}: {error}", path.display())
112 })?;
113 Ok(Self { tokenizer })
114 }
115}
116
117impl TokenizerWorker for HfTokenizerWorker {
118 fn encode(&mut self, text: &str) -> Result<Vec<u32>> {
119 let encoding = self
120 .tokenizer
121 .encode(text, false)
122 .map_err(|error| anyhow!("failed to tokenize input: {error}"))?;
123 Ok(encoding.get_ids().to_vec())
124 }
125}
126
127fn resolve_tokenizer_path(model_or_path: &str) -> Result<PathBuf> {
128 let path = Path::new(model_or_path);
129 if path.is_file() {
130 return Ok(path.to_path_buf());
131 }
132
133 if path.is_dir() {
134 let tokenizer_path = path.join("tokenizer.json");
135 if tokenizer_path.exists() {
136 return Ok(tokenizer_path);
137 }
138 bail!(
139 "directory '{}' does not contain tokenizer.json",
140 path.display()
141 );
142 }
143
144 let cache = hf_hub::Cache::default();
145 let api = hf_hub::api::sync::ApiBuilder::from_cache(cache)
146 .with_progress(true)
147 .build()
148 .context("failed to create HuggingFace API client")?;
149 let repo = api.model(model_or_path.to_string());
150 let tokenizer_path = repo
151 .get("tokenizer.json")
152 .with_context(|| format!("failed to download tokenizer.json from '{model_or_path}'"))?;
153 Ok(tokenizer_path)
154}
155
156pub(crate) fn last_word_overlap_start(text: &str, overlap_words: usize) -> usize {
157 if overlap_words == 0 || text.is_empty() {
158 return text.len();
159 }
160
161 let mut starts = Vec::new();
162 let mut in_word = false;
163 for (index, ch) in text.char_indices() {
164 if ch.is_whitespace() {
165 in_word = false;
166 continue;
167 }
168 if !in_word {
169 starts.push(index);
170 in_word = true;
171 }
172 }
173
174 if starts.len() <= overlap_words {
175 return 0;
176 }
177 starts[starts.len() - overlap_words]
178}
179
180#[cfg(test)]
181mod tests {
182 use super::{TokenizerWorker, last_word_overlap_start};
183 use anyhow::Result;
184 use std::thread;
185 use std::time::Duration;
186
187 struct StubWorker;
188
189 impl TokenizerWorker for StubWorker {
190 fn encode(&mut self, text: &str) -> Result<Vec<u32>> {
191 if text.contains("slow") {
192 thread::sleep(Duration::from_millis(5));
193 }
194 Ok(text
195 .split_whitespace()
196 .map(|word| word.len() as u32)
197 .collect())
198 }
199 }
200
201 #[test]
202 fn overlap_start_returns_zero_for_short_text() {
203 assert_eq!(last_word_overlap_start("one two", 10), 0);
204 }
205
206 #[test]
207 fn overlap_encoding_falls_back_on_prefix_break() {
208 let mut worker = StubWorker;
209 let tokens = worker
210 .encode_with_word_overlap("a b c", Some("z y"), Some(&[1, 2]), 2)
211 .unwrap();
212 assert_eq!(tokens, vec![1, 1, 1]);
213 }
214}