Skip to main content

dynamo_bench/coding/
tokenizer.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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
64/// A worker created on its first encode, so an export whose turns all carry usage loads no
65/// tokenizer.
66pub 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}