Skip to main content

logicaffeine_compile/model/hf/
tiktoken.rs

1//! A tiktoken vocabulary (Kimi K2's `tiktoken.model`) as its tokenizer encodes with it and as the Hugging Face converter
2//! reads it.
3//!
4//! `tiktoken.model` lists every mergeable token, base64, beside its rank. The tokenizer's code (`tokenization_kimi.py`)
5//! names the split pattern and the count of special tokens reserved after the ranks, each named by
6//! `tokenizer_config.json`'s `added_tokens_decoder` or `<|reserved_token_<id>|>`. Its encode cuts the text every
7//! 400,000 characters and wherever a run of spaces or of non-spaces passes 25,000, then runs `tiktoken`'s: special
8//! tokens' text read as those tokens, the rest split by the pattern, each piece the ranks hold one token and any other
9//! merged pairwise, the lowest-ranked adjacent pair first, until no pair merges.
10//!
11//! The converter's Kimi path (`DeepseekV2Model.set_vocab`) writes each token in GPT-2's byte alphabet and each merge
12//! as the pair byte-pair merging below the token's own rank leaves (`QwenModel.bpe`), and names the pre-tokenizer by
13//! the digest of the tokenizer's encoding of its probe text.
14
15use super::encode::byte_char;
16use super::onig::Regex;
17use serde_json::Value;
18use std::collections::HashMap;
19use std::io;
20use std::path::Path;
21
22fn invalid(why: impl Into<String>) -> io::Error {
23    io::Error::new(io::ErrorKind::InvalidData, why.into())
24}
25
26/// The characters per encode call Kimi's tokenizer cuts a text into (`TIKTOKEN_MAX_ENCODE_CHARS`).
27const MAX_ENCODE_CHARS: usize = 400_000;
28/// The longest run of spaces or of non-spaces it encodes whole (`MAX_NO_WHITESPACES_CHARS`).
29const MAX_RUN_CHARS: usize = 25_000;
30
31/// A tiktoken tokenizer read from its directory.
32pub struct Tiktoken {
33    /// The mergeable tokens in the file's order, with their ranks.
34    tokens: Vec<(Vec<u8>, u32)>,
35    ranks: HashMap<Vec<u8>, u32>,
36    pattern: Regex,
37    /// The special tokens' text and ids, in id order.
38    specials: Vec<(String, u32)>,
39}
40
41impl Tiktoken {
42    /// The tiktoken tokenizer of `dir`: `tiktoken.model`, the pattern and reserved count its tokenizer code states, and
43    /// the special tokens `tokenizer_config.json` names.
44    pub fn open(dir: &Path) -> io::Result<Tiktoken> {
45        let config = read_json(&dir.join("tokenizer_config.json"))?
46            .ok_or_else(|| invalid(format!("{}: no tokenizer_config.json", dir.display())))?;
47        let module = config
48            .get("auto_map")
49            .and_then(|m| m.get("AutoTokenizer"))
50            .and_then(|a| a.get(0).or(Some(a)))
51            .and_then(Value::as_str)
52            .and_then(|s| s.split_once('.'))
53            .map(|(module, _)| module.to_string())
54            .ok_or_else(|| invalid(format!("{}: tokenizer_config.json names no tokenizer code (auto_map)", dir.display())))?;
55        let code_path = dir.join(format!("{module}.py"));
56        let code = std::fs::read_to_string(&code_path).map_err(|e| invalid(format!("{}: {e}", code_path.display())))?;
57        let (pattern, reserved) = pattern_of(&code).map_err(|e| invalid(format!("{}: {e}", code_path.display())))?;
58        let pattern = Regex::tiktoken(&pattern).map_err(|e| invalid(format!("{}: {e}", code_path.display())))?;
59
60        let model_path = dir.join("tiktoken.model");
61        let text = std::fs::read_to_string(&model_path).map_err(|e| invalid(format!("{}: {e}", model_path.display())))?;
62        let mut tokens = Vec::new();
63        for (n, line) in text.lines().enumerate().filter(|(_, l)| !l.is_empty()) {
64            let mut fields = line.split_whitespace();
65            let (Some(b64), Some(rank), None) = (fields.next(), fields.next(), fields.next()) else {
66                return Err(invalid(format!("{}: line {} is not `<base64> <rank>`", model_path.display(), n + 1)));
67            };
68            let bytes = base64(b64).ok_or_else(|| invalid(format!("{}: line {}: bad base64", model_path.display(), n + 1)))?;
69            let rank = rank.parse().map_err(|_| invalid(format!("{}: line {}: bad rank", model_path.display(), n + 1)))?;
70            tokens.push((bytes, rank));
71        }
72        // `load_tiktoken_bpe` builds a dict: a token listed twice keeps its last rank, at its first place.
73        let mut ranks: HashMap<Vec<u8>, u32> = HashMap::new();
74        let mut order: Vec<Vec<u8>> = Vec::new();
75        for (bytes, rank) in tokens {
76            if ranks.insert(bytes.clone(), rank).is_none() {
77                order.push(bytes);
78            }
79        }
80        let tokens: Vec<(Vec<u8>, u32)> = order
81            .into_iter()
82            .map(|b| {
83                let rank = ranks[&b];
84                (b, rank)
85            })
86            .collect();
87
88        let named: HashMap<u32, String> = match config.get("added_tokens_decoder") {
89            Some(Value::Object(d)) => d
90                .iter()
91                .filter_map(|(id, t)| Some((id.parse().ok()?, t.get("content")?.as_str()?.to_string())))
92                .collect(),
93            _ => HashMap::new(),
94        };
95        let base = tokens.len() as u32;
96        let specials: Vec<(String, u32)> = (base..base + reserved)
97            .map(|id| (named.get(&id).cloned().unwrap_or_else(|| format!("<|reserved_token_{id}|>")), id))
98            .collect();
99        Ok(Tiktoken { tokens, ranks, pattern, specials })
100    }
101
102    /// The mergeable tokens, in the file's order, with their ranks.
103    pub fn tokens(&self) -> &[(Vec<u8>, u32)] {
104        &self.tokens
105    }
106
107    /// The special tokens' text and ids, in id order.
108    pub fn specials(&self) -> &[(String, u32)] {
109        &self.specials
110    }
111
112    /// `text` encoded as the tokenizer's `encode(text)` encodes it, its special tokens allowed.
113    pub fn encode(&self, text: &str) -> Vec<u32> {
114        let chars: Vec<char> = text.chars().collect();
115        let mut out = Vec::new();
116        for chunk in chars.chunks(MAX_ENCODE_CHARS) {
117            for piece in runs_split(chunk, MAX_RUN_CHARS) {
118                self.encode_with_specials(&piece.iter().collect::<String>(), &mut out);
119            }
120        }
121        out
122    }
123
124    /// `tiktoken`'s `encode(text, allowed_special="all")`: each special token's text, leftmost first (the longest of
125    /// several at one place), read as the token; the text between encoded ordinarily.
126    fn encode_with_specials(&self, text: &str, out: &mut Vec<u32>) {
127        let mut start = 0;
128        while start <= text.len() {
129            let next = self
130                .specials
131                .iter()
132                .filter_map(|(s, id)| text[start..].find(s.as_str()).map(|at| (start + at, s.len(), *id)))
133                .min_by_key(|&(at, len, _)| (at, std::cmp::Reverse(len)));
134            let end = next.map_or(text.len(), |(at, _, _)| at);
135            self.encode_ordinary(&text[start..end], out);
136            match next {
137                Some((at, len, id)) => {
138                    out.push(id);
139                    start = at + len;
140                }
141                None => break,
142            }
143        }
144    }
145
146    /// The text split by the pattern, each piece a token or byte-pair merged.
147    fn encode_ordinary(&self, text: &str, out: &mut Vec<u32>) {
148        let cps: Vec<u32> = text.chars().map(u32::from).collect();
149        for (lo, hi) in self.pattern.find_iter(&cps) {
150            let piece: String = cps[lo..hi].iter().filter_map(|&c| char::from_u32(c)).collect();
151            match self.ranks.get(piece.as_bytes()) {
152                Some(&rank) => out.push(rank),
153                None => out.extend(byte_pair_encode(piece.as_bytes(), &self.ranks)),
154            }
155        }
156    }
157
158    /// The pair byte-pair merging below `rank` leaves of `token` (`QwenModel.bpe(ranks, token, max_rank=rank)`), when it
159    /// leaves two parts.
160    pub fn merge_of(&self, token: &[u8], rank: u32) -> Option<(Vec<u8>, Vec<u8>)> {
161        let mut parts: Vec<Vec<u8>> = token.iter().map(|&b| vec![b]).collect();
162        loop {
163            let mut best: Option<(usize, u32)> = None;
164            for i in 0..parts.len().saturating_sub(1) {
165                let pair = [parts[i].as_slice(), parts[i + 1].as_slice()].concat();
166                if let Some(&r) = self.ranks.get(&pair) {
167                    if best.is_none_or(|(_, b)| r < b) {
168                        best = Some((i, r));
169                    }
170                }
171            }
172            match best {
173                Some((i, r)) if r < rank => {
174                    let right = parts.remove(i + 1);
175                    parts[i].extend(right);
176                }
177                _ => break,
178            }
179        }
180        (parts.len() == 2).then(|| (parts[0].clone(), parts[1].clone()))
181    }
182}
183
184/// The pieces `tiktoken`'s pattern `pattern` finds in `text`, in order.
185pub fn split(pattern: &str, text: &str) -> Result<Vec<String>, String> {
186    let re = Regex::tiktoken(pattern)?;
187    let cps: Vec<u32> = text.chars().map(u32::from).collect();
188    Ok(re.find_iter(&cps).into_iter().map(|(lo, hi)| cps[lo..hi].iter().filter_map(|&c| char::from_u32(c)).collect()).collect())
189}
190
191/// `bytes` in GPT-2's byte alphabet (`QwenModel.token_bytes_to_string`).
192pub fn byte_level(bytes: &[u8]) -> String {
193    bytes.iter().map(|&b| byte_char(b)).collect()
194}
195
196/// `tiktoken`'s `byte_pair_encode`: a piece of one byte its rank; any other merged at its lowest-ranked adjacent pair
197/// (the leftmost of equals) until none of its pairs is ranked.
198fn byte_pair_encode(piece: &[u8], ranks: &HashMap<Vec<u8>, u32>) -> Vec<u32> {
199    if piece.len() == 1 {
200        return vec![ranks[piece]];
201    }
202    let rank_of = |a: usize, b: usize| ranks.get(&piece[a..b]).copied().unwrap_or(u32::MAX);
203    // `(start, rank of the pair starting here)`, the last two sentinels.
204    let mut parts: Vec<(usize, u32)> = (0..piece.len() - 1).map(|i| (i, rank_of(i, i + 2))).collect();
205    parts.push((piece.len() - 1, u32::MAX));
206    parts.push((piece.len(), u32::MAX));
207    loop {
208        let (mut min, mut at) = (u32::MAX, usize::MAX);
209        for (i, &(_, r)) in parts[..parts.len() - 1].iter().enumerate() {
210            if r < min {
211                (min, at) = (r, i);
212            }
213        }
214        if min == u32::MAX {
215            break;
216        }
217        let pair_rank = |parts: &[(usize, u32)], i: usize| {
218            if i + 3 < parts.len() {
219                rank_of(parts[i].0, parts[i + 3].0)
220            } else {
221                u32::MAX
222            }
223        };
224        if at > 0 {
225            parts[at - 1].1 = pair_rank(&parts, at - 1);
226        }
227        parts[at].1 = pair_rank(&parts, at);
228        parts.remove(at + 1);
229    }
230    parts.windows(2).map(|w| ranks[&piece[w[0].0..w[1].0]]).collect()
231}
232
233/// Kimi's `_split_whitespaces_or_nonwhitespaces`: `s` cut wherever a run of spaces or of non-spaces (Python's
234/// `str.isspace`) would pass `max` characters.
235fn runs_split(s: &[char], max: usize) -> Vec<&[char]> {
236    let mut out = Vec::new();
237    let mut run = 0;
238    let mut in_space = s.first().is_some_and(|&c| python_space(c));
239    let mut start = 0;
240    for (i, &c) in s.iter().enumerate() {
241        let space = python_space(c);
242        if in_space != space {
243            run = 1;
244            in_space = space;
245        } else {
246            run += 1;
247            if run > max {
248                out.push(&s[start..i]);
249                start = i;
250                run = 1;
251            }
252        }
253    }
254    out.push(&s[start..]);
255    out
256}
257
258/// Python's `str.isspace` of one character: bidirectional class WS, B or S, or general category Zs.
259fn python_space(c: char) -> bool {
260    matches!(
261        c as u32,
262        0x09..=0x0D | 0x1C..=0x20 | 0x85 | 0xA0 | 0x1680 | 0x2000..=0x200A | 0x2028 | 0x2029 | 0x202F | 0x205F | 0x3000
263    )
264}
265
266/// The split pattern (`pat_str = "|".join([r"""…""", …])`) and the reserved special-token count
267/// (`num_reserved_special_tokens = <n>`) a tiktoken tokenizer's code states.
268fn pattern_of(code: &str) -> Result<(String, u32), String> {
269    let start = code.find("pat_str = \"|\".join(").ok_or("no `pat_str = \"|\".join(` in the tokenizer code")?;
270    let body = &code[start..];
271    let end = body.find("])").ok_or("the pattern's list is not closed")?;
272    let mut alternatives = Vec::new();
273    let mut rest = &body[..end];
274    while let Some(open) = rest.find("r\"\"\"") {
275        let after = &rest[open + 4..];
276        let close = after.find("\"\"\"").ok_or("an unclosed raw string in the pattern")?;
277        alternatives.push(&after[..close]);
278        rest = &after[close + 3..];
279    }
280    if alternatives.is_empty() {
281        return Err("the pattern's list holds no raw strings".into());
282    }
283    let reserved = code
284        .lines()
285        .find_map(|l| l.trim().strip_prefix("num_reserved_special_tokens = "))
286        .ok_or("no `num_reserved_special_tokens` in the tokenizer code")?
287        .trim()
288        .parse()
289        .map_err(|_| "num_reserved_special_tokens is not a count")?;
290    Ok((alternatives.join("|"), reserved))
291}
292
293/// Standard base64 (RFC 4648), its padding optional.
294fn base64(s: &str) -> Option<Vec<u8>> {
295    let value = |c: u8| -> Option<u32> {
296        Some(match c {
297            b'A'..=b'Z' => c - b'A',
298            b'a'..=b'z' => c - b'a' + 26,
299            b'0'..=b'9' => c - b'0' + 52,
300            b'+' => 62,
301            b'/' => 63,
302            _ => return None,
303        } as u32)
304    };
305    let digits: Vec<u8> = s.bytes().filter(|&c| c != b'=').collect();
306    if digits.len() % 4 == 1 {
307        return None;
308    }
309    let mut out = Vec::with_capacity(digits.len() * 3 / 4);
310    for group in digits.chunks(4) {
311        let mut acc = 0u32;
312        for (i, &c) in group.iter().enumerate() {
313            acc |= value(c)? << (18 - 6 * i);
314        }
315        out.extend_from_slice(&acc.to_be_bytes()[1..group.len()]);
316    }
317    Some(out)
318}
319
320fn read_json(path: &Path) -> io::Result<Option<Value>> {
321    match std::fs::read_to_string(path) {
322        Ok(s) => serde_json::from_str(&s).map(Some).map_err(|e| invalid(format!("{}: {e}", path.display()))),
323        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
324        Err(e) => Err(e),
325    }
326}