pub fn should_add_bos_token(file: &impl ferrox_gguf::TensorSource) -> bool {
if let Some(v) = file.metadata_bool("tokenizer.ggml.add_bos_token") {
return v;
}
let model = file.metadata_str("tokenizer.ggml.model").unwrap_or("");
let pre = file.metadata_str("tokenizer.ggml.pre").unwrap_or("");
if matches!(model, "llama" | "spm") || model.contains("sentencepiece") {
return true;
}
matches!(pre, "tekken" | "chameleon")
}
const EOG_TOKEN_TEXTS: &[&str] = &[
"<|eot_id|>",
"<|im_end|>",
"<|end|>",
"<|return|>", "<|call|>", "<|flush|>", "<|calls|>", "<end_of_turn>",
"<|endoftext|>",
"</s>", "<|eom_id|>",
"<EOT>",
"_<EOT>",
"[EOT]", "[EOS]", "<|end_of_text|>",
"<end_of_utterance>", "<eos>", "<turn|>", "<|tool_response>", "<|end▁of▁sentence|>", "[e~[", ];
pub fn eog_token_ids(file: &impl ferrox_gguf::TensorSource) -> std::collections::HashSet<u32> {
let mut out = std::collections::HashSet::new();
for key in [
"tokenizer.ggml.eos_token_id",
"tokenizer.ggml.eot_token_id",
"tokenizer.ggml.eom_token_id",
] {
if let Some(id) = file.metadata_u64(key) {
out.insert(id as u32);
}
}
if let Some(ferrox_gguf::GgufValue::Array(items)) = file.metadata("tokenizer.ggml.tokens") {
for (id, v) in items.iter().enumerate() {
if let ferrox_gguf::GgufValue::String(text) = v {
if EOG_TOKEN_TEXTS.contains(&text.as_str()) {
out.insert(id as u32);
}
}
}
}
out
}
pub struct ByteTokenizer;
impl ByteTokenizer {
pub fn encode(text: &str) -> Vec<u32> {
text.bytes().map(|b| b as u32).collect()
}
pub fn decode(ids: &[u32]) -> String {
let bytes: Vec<u8> = ids.iter().filter_map(|&id| u8::try_from(id).ok()).collect();
String::from_utf8_lossy(&bytes).into_owned()
}
pub const VOCAB_SIZE: usize = 256;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum BpeEncodingStyle {
Gpt2,
SpmWhitespace,
}
fn gpt2_byte_to_unicode() -> ([char; 256], std::collections::HashMap<char, u8>) {
let is_printable =
|b: u16| (33..=126).contains(&b) || (161..=172).contains(&b) || (174..=255).contains(&b);
let mut forward = ['\0'; 256];
let mut extra_offset = 0u32;
for b in 0..256u16 {
if is_printable(b) {
forward[b as usize] = char::from_u32(b as u32).unwrap();
} else {
forward[b as usize] = char::from_u32(256 + extra_offset).unwrap();
extra_offset += 1;
}
}
let mut reverse = std::collections::HashMap::with_capacity(256);
for (b, &c) in forward.iter().enumerate() {
reverse.insert(c, b as u8);
}
(forward, reverse)
}
fn gpt2_pretokenize_regex() -> regex::Regex {
regex::Regex::new(r"'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+")
.expect("GPT2 pre-tokenization pattern is a fixed, valid regex")
}
fn newline_pretokenize_regex() -> regex::Regex {
regex::Regex::new(r"[^\n]+|[\n]+").expect("newline pretokenize pattern is fixed")
}
const SPM_SPACE: char = '\u{2581}';
pub struct GgufBpeTokenizer {
token_to_id: std::collections::HashMap<String, u32>,
id_to_token: Vec<String>,
merge_rank: std::collections::HashMap<(String, String), usize>,
byte_to_unicode: [char; 256],
unicode_to_byte: std::collections::HashMap<char, u8>,
special_tokens: Vec<(String, u32)>,
pretokenize_pattern: regex::Regex,
style: BpeEncodingStyle,
}
#[derive(Debug, thiserror::Error)]
pub enum TokenizerLoadError {
#[error("GGUF file has no 'tokenizer.ggml.tokens' metadata array")]
MissingTokens,
#[error("'tokenizer.ggml.tokens' is present but is not a string array")]
TokensNotStringArray,
}
const GGML_TOKEN_TYPE_CONTROL: i64 = 3;
const GGML_TOKEN_TYPE_USER_DEFINED: i64 = 4;
fn load_special_tokens(
file: &impl ferrox_gguf::TensorSource,
id_to_token: &[String],
) -> Vec<(String, u32)> {
let Some(ferrox_gguf::GgufValue::Array(items)) = file.metadata("tokenizer.ggml.token_type")
else {
return Vec::new();
};
items
.iter()
.zip(id_to_token.iter())
.enumerate()
.filter_map(|(id, (v, text))| {
let ty = match v {
ferrox_gguf::GgufValue::I32(t) => *t as i64,
ferrox_gguf::GgufValue::U32(t) => *t as i64,
_ => return None,
};
(ty == GGML_TOKEN_TYPE_CONTROL || ty == GGML_TOKEN_TYPE_USER_DEFINED)
.then(|| (text.clone(), id as u32))
})
.collect()
}
enum TextOrSpecial<'a> {
Text(&'a str),
Special(u32),
}
fn split_on_special_tokens<'a>(
text: &'a str,
specials: &[(String, u32)],
) -> Vec<TextOrSpecial<'a>> {
if specials.is_empty() {
return vec![TextOrSpecial::Text(text)];
}
let mut segments = Vec::new();
let mut pos = 0usize;
while pos < text.len() {
let mut best: Option<(usize, usize, u32)> = None; for (s, id) in specials {
if s.is_empty() {
continue;
}
if let Some(rel) = text[pos..].find(s.as_str()) {
let start = pos + rel;
let len = s.len();
let better = match best {
None => true,
Some((bstart, blen, _)) => start < bstart || (start == bstart && len > blen),
};
if better {
best = Some((start, len, *id));
}
}
}
match best {
None => break,
Some((start, len, id)) => {
if start > pos {
segments.push(TextOrSpecial::Text(&text[pos..start]));
}
segments.push(TextOrSpecial::Special(id));
pos = start + len;
}
}
}
if pos < text.len() {
segments.push(TextOrSpecial::Text(&text[pos..]));
}
segments
}
#[cfg(test)]
mod special_token_split_tests {
use super::*;
fn text_of<'a>(seg: &TextOrSpecial<'a>) -> Option<&'a str> {
match seg {
TextOrSpecial::Text(t) => Some(t),
TextOrSpecial::Special(_) => None,
}
}
#[test]
fn empty_specials_list_returns_the_whole_text_unsplit() {
let segs = split_on_special_tokens("hello world", &[]);
assert_eq!(segs.len(), 1);
assert_eq!(text_of(&segs[0]), Some("hello world"));
}
#[test]
fn splits_around_a_single_special_token_in_the_middle() {
let specials = vec![("<|user|>".to_string(), 42u32)];
let segs = split_on_special_tokens("before<|user|>after", &specials);
assert_eq!(segs.len(), 3);
assert_eq!(text_of(&segs[0]), Some("before"));
assert!(matches!(segs[1], TextOrSpecial::Special(42)));
assert_eq!(text_of(&segs[2]), Some("after"));
}
#[test]
fn multiple_occurrences_and_multiple_distinct_specials_all_split() {
let specials = vec![
("<|user|>".to_string(), 1u32),
("<|assistant|>".to_string(), 2u32),
];
let segs = split_on_special_tokens("<|user|>hi<|assistant|>hello<|user|>bye", &specials);
let kinds: Vec<Option<&str>> = segs.iter().map(text_of).collect();
assert_eq!(
kinds,
vec![None, Some("hi"), None, Some("hello"), None, Some("bye")]
);
assert!(matches!(segs[0], TextOrSpecial::Special(1)));
assert!(matches!(segs[2], TextOrSpecial::Special(2)));
assert!(matches!(segs[4], TextOrSpecial::Special(1)));
}
#[test]
fn longest_match_wins_on_a_tied_start_position() {
let specials = vec![("<|user|>".to_string(), 1u32), ("<|u".to_string(), 99u32)];
let segs = split_on_special_tokens("<|user|>x", &specials);
assert!(matches!(segs[0], TextOrSpecial::Special(1)));
}
#[test]
fn no_match_at_all_returns_the_whole_text_as_one_segment() {
let specials = vec![("<|user|>".to_string(), 1u32)];
let segs = split_on_special_tokens("plain text with no specials", &specials);
assert_eq!(segs.len(), 1);
assert_eq!(text_of(&segs[0]), Some("plain text with no specials"));
}
}
impl GgufBpeTokenizer {
pub fn from_gguf(file: &impl ferrox_gguf::TensorSource) -> Result<Self, TokenizerLoadError> {
let tokens_value = file
.metadata("tokenizer.ggml.tokens")
.ok_or(TokenizerLoadError::MissingTokens)?;
let id_to_token: Vec<String> = match tokens_value {
ferrox_gguf::GgufValue::Array(items) => items
.iter()
.map(|v| v.as_str().map(|s| s.to_string()))
.collect::<Option<Vec<_>>>()
.ok_or(TokenizerLoadError::TokensNotStringArray)?,
_ => return Err(TokenizerLoadError::TokensNotStringArray),
};
let token_to_id: std::collections::HashMap<String, u32> = id_to_token
.iter()
.enumerate()
.map(|(i, t)| (t.clone(), i as u32))
.collect();
let style = match file.metadata_str("tokenizer.ggml.model") {
Some("gemma4") => BpeEncodingStyle::SpmWhitespace,
_ => BpeEncodingStyle::Gpt2,
};
let mut merge_rank = std::collections::HashMap::new();
if let Some(ferrox_gguf::GgufValue::Array(items)) = file.metadata("tokenizer.ggml.merges") {
for (rank, item) in items.iter().enumerate() {
if let Some(s) = item.as_str() {
if let Some((a, b)) = split_bpe_merge_pair(s, style) {
merge_rank.insert((a, b), rank);
}
}
}
}
let (byte_to_unicode, unicode_to_byte) = gpt2_byte_to_unicode();
let pretokenize_pattern = match style {
BpeEncodingStyle::Gpt2 => gpt2_pretokenize_regex(),
BpeEncodingStyle::SpmWhitespace => newline_pretokenize_regex(),
};
let special_tokens = load_special_tokens(file, &id_to_token);
Ok(GgufBpeTokenizer {
token_to_id,
id_to_token,
merge_rank,
byte_to_unicode,
unicode_to_byte,
special_tokens,
pretokenize_pattern,
style,
})
}
pub fn vocab_size(&self) -> usize {
self.id_to_token.len()
}
pub fn has_merges(&self) -> bool {
!self.merge_rank.is_empty()
}
pub fn encode_word(&self, word: &str) -> Vec<u32> {
let mut pieces: Vec<String> = match self.style {
BpeEncodingStyle::Gpt2 => word
.bytes()
.map(|b| self.byte_to_unicode[b as usize].to_string())
.collect(),
BpeEncodingStyle::SpmWhitespace => word.chars().map(|c| c.to_string()).collect(),
};
if pieces.is_empty() {
return Vec::new();
}
loop {
let mut best: Option<(usize, usize)> = None; for i in 0..pieces.len().saturating_sub(1) {
if let Some(&rank) = self
.merge_rank
.get(&(pieces[i].clone(), pieces[i + 1].clone()))
{
if best.map(|(r, _)| rank < r).unwrap_or(true) {
best = Some((rank, i));
}
}
}
match best {
Some((_, i)) => {
let merged = format!("{}{}", pieces[i], pieces[i + 1]);
pieces.splice(i..=i + 1, [merged]);
}
None => break,
}
}
pieces.iter().flat_map(|p| self.piece_to_ids(p)).collect()
}
fn piece_to_ids(&self, piece: &str) -> Vec<u32> {
if let Some(&id) = self.token_to_id.get(piece) {
return vec![id];
}
match self.style {
BpeEncodingStyle::Gpt2 => {
piece
.chars()
.next()
.and_then(|c| self.token_to_id.get(&c.to_string()))
.copied()
.map(|id| vec![id])
.unwrap_or_else(|| vec![0])
}
BpeEncodingStyle::SpmWhitespace => {
piece
.bytes()
.filter_map(|b| {
let hex = format!("<0x{b:02X}>");
self.token_to_id.get(&hex).copied()
})
.collect()
}
}
}
pub fn encode(&self, text: &str) -> Vec<u32> {
split_on_special_tokens(text, &self.special_tokens)
.into_iter()
.flat_map(|seg| -> Vec<u32> {
match seg {
TextOrSpecial::Special(id) => vec![id],
TextOrSpecial::Text(t) => self.encode_text_run(t),
}
})
.collect()
}
fn encode_text_run(&self, text: &str) -> Vec<u32> {
match self.style {
BpeEncodingStyle::Gpt2 => self
.pretokenize_pattern
.find_iter(text)
.flat_map(|m| self.encode_word(m.as_str()))
.collect(),
BpeEncodingStyle::SpmWhitespace => {
let escaped: String = text
.chars()
.map(|c| if c == ' ' { SPM_SPACE } else { c })
.collect();
let mut out = Vec::new();
let bytes = escaped.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
let is_nl = bytes[i] == b'\n';
let mut j = i + 1;
while j < bytes.len() && (bytes[j] == b'\n') == is_nl {
j += 1;
}
let word = std::str::from_utf8(&bytes[i..j]).expect("newline split keeps utf8");
if is_nl {
if let Some(&id) = self.token_to_id.get(word) {
out.push(id);
} else {
out.extend(self.encode_word(word));
}
} else {
out.extend(self.encode_word(word));
}
i = j;
}
out
}
}
}
pub fn decode(&self, ids: &[u32]) -> String {
match self.style {
BpeEncodingStyle::Gpt2 => {
let bytes: Vec<u8> = ids
.iter()
.filter_map(|&id| self.id_to_token.get(id as usize))
.flat_map(|token| token.chars())
.filter_map(|c| self.unicode_to_byte.get(&c).copied())
.collect();
String::from_utf8_lossy(&bytes).into_owned()
}
BpeEncodingStyle::SpmWhitespace => {
let mut bytes: Vec<u8> = Vec::new();
for &id in ids {
let Some(token) = self.id_to_token.get(id as usize) else {
continue;
};
if let Some(b) = spm_byte_fallback_value(token) {
bytes.push(b);
} else {
bytes.extend(token.replace(SPM_SPACE, " ").into_bytes());
}
}
String::from_utf8_lossy(&bytes).into_owned()
}
}
}
}
fn split_bpe_merge_pair(s: &str, style: BpeEncodingStyle) -> Option<(String, String)> {
match style {
BpeEncodingStyle::Gpt2 => s
.split_once(' ')
.map(|(a, b)| (a.to_string(), b.to_string())),
BpeEncodingStyle::SpmWhitespace => {
let bytes = s.as_bytes();
if bytes.len() < 2 {
return None;
}
let pos = bytes[1..].iter().position(|&b| b == b' ')? + 1;
Some((s[..pos].to_string(), s[pos + 1..].to_string()))
}
}
}
fn spm_byte_fallback_value(token: &str) -> Option<u8> {
let hex = token.strip_prefix("<0x")?.strip_suffix('>')?;
if hex.len() != 2 {
return None;
}
u8::from_str_radix(hex, 16).ok()
}
pub struct GgufSpmTokenizer {
token_to_id: std::collections::HashMap<String, u32>,
id_to_token: Vec<String>,
scores: Vec<f32>,
special_tokens: Vec<(String, u32)>,
add_space_prefix: bool,
}
struct SpmMergeCandidate {
score: f32,
left: usize,
right: usize,
insertion_order: u64,
expected_left_text: String,
expected_right_text: String,
}
impl PartialEq for SpmMergeCandidate {
fn eq(&self, other: &Self) -> bool {
self.score == other.score && self.insertion_order == other.insertion_order
}
}
impl Eq for SpmMergeCandidate {}
impl PartialOrd for SpmMergeCandidate {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SpmMergeCandidate {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.score
.partial_cmp(&other.score)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| other.left.cmp(&self.left))
.then_with(|| other.insertion_order.cmp(&self.insertion_order))
}
}
impl GgufSpmTokenizer {
pub fn from_gguf(file: &impl ferrox_gguf::TensorSource) -> Result<Self, TokenizerLoadError> {
let tokens_value = file
.metadata("tokenizer.ggml.tokens")
.ok_or(TokenizerLoadError::MissingTokens)?;
let id_to_token: Vec<String> = match tokens_value {
ferrox_gguf::GgufValue::Array(items) => items
.iter()
.map(|v| v.as_str().map(|s| s.to_string()))
.collect::<Option<Vec<_>>>()
.ok_or(TokenizerLoadError::TokensNotStringArray)?,
_ => return Err(TokenizerLoadError::TokensNotStringArray),
};
let token_to_id: std::collections::HashMap<String, u32> = id_to_token
.iter()
.enumerate()
.map(|(i, t)| (t.clone(), i as u32))
.collect();
let scores: Vec<f32> = match file.metadata("tokenizer.ggml.scores") {
Some(ferrox_gguf::GgufValue::Array(items)) => {
items.iter().map(|v| v.as_f32().unwrap_or(0.0)).collect()
}
_ => vec![0.0; id_to_token.len()],
};
let special_tokens = load_special_tokens(file, &id_to_token);
let add_space_prefix = match file.metadata("tokenizer.ggml.add_space_prefix") {
Some(ferrox_gguf::GgufValue::Bool(v)) => *v,
_ => true,
};
Ok(GgufSpmTokenizer {
token_to_id,
id_to_token,
scores,
special_tokens,
add_space_prefix,
})
}
pub fn vocab_size(&self) -> usize {
self.id_to_token.len()
}
pub fn encode(&self, text: &str) -> Vec<u32> {
split_on_special_tokens(text, &self.special_tokens)
.into_iter()
.flat_map(|seg| match seg {
TextOrSpecial::Special(id) => vec![id],
TextOrSpecial::Text(t) => self.encode_normal_run(t),
})
.collect()
}
fn encode_normal_run(&self, text: &str) -> Vec<u32> {
let replaced: String = text
.chars()
.map(|c| if c == ' ' { '\u{2581}' } else { c })
.collect();
let normalized = if self.add_space_prefix {
format!("\u{2581}{replaced}")
} else {
replaced
};
let mut symbols: Vec<String> = Vec::new();
for ch in normalized.chars() {
let s = ch.to_string();
if self.token_to_id.contains_key(&s) {
symbols.push(s);
} else {
for byte in s.as_bytes() {
symbols.push(format!("<0x{byte:02X}>"));
}
}
}
let n = symbols.len();
if n == 0 {
return Vec::new();
}
let mut nexts: Vec<Option<usize>> = (1..=n)
.map(|i| if i < n { Some(i) } else { None })
.collect();
let mut prevs: Vec<Option<usize>> = (0..n)
.map(|i| if i == 0 { None } else { Some(i - 1) })
.collect();
let mut alive = vec![true; n];
let mut heap: std::collections::BinaryHeap<SpmMergeCandidate> =
std::collections::BinaryHeap::new();
let mut insertion_order = 0u64;
let try_add_merge = |l: Option<usize>,
r: Option<usize>,
symbols: &[String],
heap: &mut std::collections::BinaryHeap<SpmMergeCandidate>,
insertion_order: &mut u64| {
let (Some(l), Some(r)) = (l, r) else { return };
let merged = format!("{}{}", symbols[l], symbols[r]);
if let Some(&id) = self.token_to_id.get(&merged) {
let score = self.scores.get(id as usize).copied().unwrap_or(0.0);
*insertion_order += 1;
heap.push(SpmMergeCandidate {
score,
left: l,
right: r,
insertion_order: *insertion_order,
expected_left_text: symbols[l].clone(),
expected_right_text: symbols[r].clone(),
});
}
};
for i in 0..n.saturating_sub(1) {
try_add_merge(
Some(i),
Some(i + 1),
&symbols,
&mut heap,
&mut insertion_order,
);
}
while let Some(candidate) = heap.pop() {
let (l, r) = (candidate.left, candidate.right);
if !alive[l] || !alive[r] {
continue;
}
if nexts[l] != Some(r) {
continue;
}
if symbols[l] != candidate.expected_left_text
|| symbols[r] != candidate.expected_right_text
{
continue; }
symbols[l] = format!("{}{}", symbols[l], symbols[r]);
alive[r] = false;
nexts[l] = nexts[r];
if let Some(next_of_r) = nexts[r] {
prevs[next_of_r] = Some(l);
}
try_add_merge(prevs[l], Some(l), &symbols, &mut heap, &mut insertion_order);
try_add_merge(Some(l), nexts[l], &symbols, &mut heap, &mut insertion_order);
}
let mut result = Vec::new();
let mut i = Some(0usize);
while let Some(idx) = i {
if alive[idx] {
result.push(self.token_to_id.get(&symbols[idx]).copied().unwrap_or(0));
}
i = nexts[idx];
}
result
}
fn byte_fallback_value(token: &str) -> Option<u8> {
let hex = token.strip_prefix("<0x")?.strip_suffix('>')?;
if hex.len() != 2 {
return None;
}
u8::from_str_radix(hex, 16).ok()
}
pub fn decode(&self, ids: &[u32]) -> String {
let mut bytes: Vec<u8> = Vec::new();
for &id in ids {
let Some(token) = self.id_to_token.get(id as usize) else {
continue;
};
if let Some(b) = Self::byte_fallback_value(token) {
bytes.push(b);
} else {
bytes.extend(token.replace('\u{2581}', " ").into_bytes());
}
}
String::from_utf8_lossy(&bytes).into_owned()
}
}
pub struct GgufUnigramTokenizer {
token_to_id: std::collections::HashMap<String, u32>,
id_to_token: Vec<String>,
scores: Vec<f32>,
unk_id: u32,
max_piece_chars: usize,
unknown_token_score: f64,
special_tokens: Vec<(String, u32)>,
}
impl GgufUnigramTokenizer {
pub fn from_gguf(file: &impl ferrox_gguf::TensorSource) -> Result<Self, TokenizerLoadError> {
let tokens_value = file
.metadata("tokenizer.ggml.tokens")
.ok_or(TokenizerLoadError::MissingTokens)?;
let id_to_token: Vec<String> = match tokens_value {
ferrox_gguf::GgufValue::Array(items) => items
.iter()
.map(|v| v.as_str().map(|s| s.to_string()))
.collect::<Option<Vec<_>>>()
.ok_or(TokenizerLoadError::TokensNotStringArray)?,
_ => return Err(TokenizerLoadError::TokensNotStringArray),
};
let token_to_id: std::collections::HashMap<String, u32> = id_to_token
.iter()
.enumerate()
.map(|(i, t)| (t.clone(), i as u32))
.collect();
let scores: Vec<f32> = match file.metadata("tokenizer.ggml.scores") {
Some(ferrox_gguf::GgufValue::Array(items)) => {
items.iter().map(|v| v.as_f32().unwrap_or(0.0)).collect()
}
_ => vec![0.0; id_to_token.len()],
};
let unk_id = file
.metadata("tokenizer.ggml.unknown_token_id")
.and_then(|v| v.as_u64())
.map(|v| v as u32)
.unwrap_or(0);
let max_piece_chars = id_to_token
.iter()
.map(|t| t.chars().count())
.max()
.unwrap_or(1)
.max(1);
let min_score = scores.iter().copied().fold(f32::INFINITY, f32::min) as f64;
let unknown_token_score = min_score - 10.0;
let special_tokens = load_special_tokens(file, &id_to_token);
Ok(GgufUnigramTokenizer {
token_to_id,
id_to_token,
scores,
unk_id,
max_piece_chars,
unknown_token_score,
special_tokens,
})
}
pub fn vocab_size(&self) -> usize {
self.id_to_token.len()
}
pub fn encode(&self, text: &str) -> Vec<u32> {
split_on_special_tokens(text, &self.special_tokens)
.into_iter()
.flat_map(|seg| match seg {
TextOrSpecial::Special(id) => vec![id],
TextOrSpecial::Text(t) => self.encode_normal_run(t),
})
.collect()
}
fn encode_normal_run(&self, text: &str) -> Vec<u32> {
let collapsed = text.split_whitespace().collect::<Vec<_>>().join(" ");
let replaced: String = collapsed
.chars()
.map(|c| if c == ' ' { '\u{2581}' } else { c })
.collect();
let normalized = format!("\u{2581}{replaced}");
let chars: Vec<char> = normalized.chars().collect();
let n = chars.len();
if n == 0 {
return Vec::new();
}
struct Best {
token_id: u32,
from: usize,
score: f64,
}
let mut dp: Vec<Best> = (0..=n)
.map(|_| Best {
token_id: 0,
from: 0,
score: f64::NEG_INFINITY,
})
.collect();
dp[0].score = 0.0;
for i in 0..n {
if dp[i].score == f64::NEG_INFINITY {
continue; }
let base = dp[i].score;
let max_len = self.max_piece_chars.min(n - i);
for len in 1..=max_len {
let piece: String = chars[i..i + len].iter().collect();
if let Some(&id) = self.token_to_id.get(&piece) {
let candidate = base + self.scores[id as usize] as f64;
let j = i + len;
if candidate > dp[j].score {
dp[j] = Best {
token_id: id,
from: i,
score: candidate,
};
}
}
}
let j = i + 1;
let candidate = base + self.unknown_token_score;
if candidate > dp[j].score {
dp[j] = Best {
token_id: self.unk_id,
from: i,
score: candidate,
};
}
}
let mut result = Vec::new();
let mut pos = n;
while pos > 0 {
result.push(dp[pos].token_id);
pos = dp[pos].from;
}
result.reverse();
result
}
pub fn decode(&self, ids: &[u32]) -> String {
let mut out = String::new();
for &id in ids {
if let Some(token) = self.id_to_token.get(id as usize) {
out.push_str(&token.replace('\u{2581}', " "));
}
}
out
}
}
#[cfg(test)]
mod gguf_vocab_tests {
use super::*;
fn load_real_fixture() -> GgufBpeTokenizer {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/fixtures/llama-bpe-vocab.gguf"
);
let file = ferrox_gguf::GgufFile::open(path).expect("real vocab fixture must open");
GgufBpeTokenizer::from_gguf(&file).expect("real vocab fixture must parse as a tokenizer")
}
#[test]
fn loads_real_downloaded_llama_bpe_vocab() {
let tok = load_real_fixture();
assert!(
tok.vocab_size() > 100_000,
"vocab_size={}",
tok.vocab_size()
);
assert!(tok.has_merges(), "llama-bpe vocab ships a real merge table");
}
#[test]
fn decode_of_known_ids_is_stable() {
let tok = load_real_fixture();
let a = tok.decode(&[0]);
let b = tok.decode(&[0]);
assert_eq!(a, b);
}
#[test]
fn encode_word_never_panics_on_arbitrary_input() {
let tok = load_real_fixture();
for word in ["hello", "", "a", "the quick brown fox", "\u{1f980}"] {
let ids = tok.encode_word(word);
let _ = tok.decode(&ids);
}
}
#[test]
fn encode_sentence_round_trips_through_real_vocab() {
let tok = load_real_fixture();
for sentence in [
"the quick brown fox jumps over the lazy dog",
"Hello, World! 123",
"ferrox is a pure-Rust inference engine.",
] {
let ids = tok.encode(sentence);
assert!(!ids.is_empty());
let decoded = tok.decode(&ids);
assert_eq!(
decoded, sentence,
"full sentence encode/decode through the pre-tokenizer must reproduce the input exactly"
);
}
}
#[test]
fn pretokenizer_splits_on_word_boundaries_not_mid_word() {
let tok = load_real_fixture();
let combined = tok.encode("cat dog");
let mut separate = tok.encode_word("cat");
separate.extend(tok.encode_word(" dog"));
assert_eq!(
combined, separate,
"pre-tokenized sentence encoding must match word-by-word encoding at real word boundaries"
);
}
#[test]
fn pretokenizer_keeps_contractions_as_gpt2_does() {
let tok = load_real_fixture();
let pieces: Vec<&str> = tok
.pretokenize_pattern
.find_iter("don't")
.map(|m| m.as_str())
.collect();
assert_eq!(pieces, vec!["don", "'t"]);
}
#[test]
fn ascii_word_round_trips_through_real_vocab_encode_decode() {
let tok = load_real_fixture();
for word in ["hello", "ferrox", "test", "quick brown fox"] {
let ids = tok.encode_word(word);
assert!(!ids.is_empty(), "encoding {word:?} produced no tokens");
let decoded = tok.decode(&ids);
assert_eq!(
decoded, word,
"round-trip through the real vocab's encode/decode should reproduce ASCII text exactly"
);
}
}
#[test]
fn multibyte_utf8_round_trips_through_real_vocab_encode_decode() {
let tok = load_real_fixture();
for word in ["caf\u{e9}", "\u{1f980}", "\u{4e2d}\u{6587}"] {
let ids = tok.encode_word(word);
let decoded = tok.decode(&ids);
assert_eq!(
decoded, word,
"byte-level BPE must round-trip arbitrary UTF-8, not just ASCII"
);
}
}
#[test]
fn gpt2_remap_matches_known_reference_points() {
let (fwd, rev) = super::gpt2_byte_to_unicode();
assert_eq!(fwd[0x21], '!');
assert_eq!(fwd[0x20], '\u{120}');
assert_eq!(rev[&'!'], 0x21);
assert_eq!(rev[&'\u{120}'], 0x20);
}
#[test]
fn real_vocab_uses_gpt2_space_remap_in_its_own_tokens() {
let tok = load_real_fixture();
let has_space_prefixed_token = tok.id_to_token.iter().any(|t| t.starts_with('\u{120}'));
assert!(
has_space_prefixed_token,
"expected at least one real vocab token starting with the GPT2 remapped-space character"
);
}
}
#[cfg(test)]
mod gguf_spm_tests {
use super::*;
fn load_real_fixture() -> GgufSpmTokenizer {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/fixtures/llama-spm-vocab.gguf"
);
let file = ferrox_gguf::GgufFile::open(path).expect("real SPM vocab fixture must open");
GgufSpmTokenizer::from_gguf(&file)
.expect("real SPM vocab fixture must parse as a tokenizer")
}
#[test]
fn loads_real_downloaded_llama_spm_vocab() {
let tok = load_real_fixture();
assert_eq!(
tok.vocab_size(),
32000,
"the real LLaMA-1/2 tokenizer vocab is exactly 32000 tokens"
);
}
#[test]
fn matches_known_reference_encodings() {
let tok = load_real_fixture();
assert_eq!(tok.encode("Hello world"), vec![15043, 3186]);
assert_eq!(tok.encode(" Hello world"), vec![29871, 15043, 3186]);
assert_eq!(tok.encode("Hello World"), vec![15043, 2787]);
}
#[test]
fn chat_template_control_tokens_are_encoded_atomically_not_shattered() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/spm-special-tokens-test-vocab.gguf"
);
let file = ferrox_gguf::GgufFile::open(path).expect("fixture must open");
let tok = GgufSpmTokenizer::from_gguf(&file).expect("fixture must parse");
let user_id = 269u32;
let assistant_id = 270u32;
let ids = tok.encode("<|user|>hello<|assistant|>");
assert_eq!(ids.first().copied(), Some(user_id), "ids={ids:?}");
assert_eq!(ids.last().copied(), Some(assistant_id), "ids={ids:?}");
assert!(
!ids[1..ids.len() - 1].contains(&user_id)
&& !ids[1..ids.len() - 1].contains(&assistant_id),
"control tokens must appear exactly once each, at the boundaries: ids={ids:?}"
);
}
#[test]
fn byte_fallback_handles_control_characters() {
let tok = load_real_fixture();
assert_eq!(
tok.encode("\t"),
vec![29871, 12],
"tab must byte-fallback to <0x09> = token 12"
);
assert_eq!(
tok.encode("\n"),
vec![29871, 13],
"newline must byte-fallback to <0x0A> = token 13"
);
}
#[test]
fn matches_llama_cpp_full_reference_test_suite_exactly() {
let tok = load_real_fixture();
let inp_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/fixtures/llama-spm-vocab.gguf.inp"
);
let out_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/fixtures/llama-spm-vocab.gguf.out"
);
let inp_raw = std::fs::read_to_string(inp_path).expect("reference .inp file must exist");
let out_raw = std::fs::read_to_string(out_path).expect("reference .out file must exist");
let marker = "__ggml_vocab_test__\n";
let mut inputs: Vec<&str> = inp_raw.split(marker).collect();
inputs.retain(|s| !s.is_empty());
let inputs: Vec<String> = inputs
.iter()
.map(|s| s.strip_suffix('\n').unwrap_or(s).to_string())
.collect();
let outputs: Vec<&str> = out_raw.split('\n').collect();
assert!(
inputs.len() >= 40,
"expected the full ~45-case reference suite, got {}",
inputs.len()
);
let mut checked = 0;
for (i, text) in inputs.iter().enumerate() {
let Some(expected_line) = outputs.get(i) else {
break;
};
let expected_line = expected_line.trim();
if expected_line.is_empty() {
continue;
}
let expected: Vec<u32> = expected_line
.split_whitespace()
.map(|s| s.parse().unwrap())
.collect();
let got = tok.encode(text);
assert_eq!(got, expected, "case #{i}: text={text:?}");
checked += 1;
}
assert!(
checked >= 40,
"expected to actually check at least 40 real cases, only checked {checked}"
);
}
#[test]
fn decode_reverses_encode_for_ascii_text() {
let tok = load_real_fixture();
let text = "Hello world";
let ids = tok.encode(text);
assert_eq!(tok.decode(&ids), " Hello world");
}
#[test]
fn decode_reverses_byte_fallback_tokens_to_the_real_raw_bytes() {
let tok = load_real_fixture();
let newline_id = tok.encode("\n");
assert_eq!(tok.decode(&newline_id), " \n");
let emoji = "🦀";
let ids = tok.encode(emoji);
assert_eq!(tok.decode(&ids), format!(" {emoji}"));
}
}
#[cfg(test)]
mod gguf_unigram_tests {
use super::*;
fn load_real_fixture() -> GgufUnigramTokenizer {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/unigram-test-vocab.gguf"
);
let file = ferrox_gguf::GgufFile::open(path).expect("real Unigram vocab fixture must open");
GgufUnigramTokenizer::from_gguf(&file)
.expect("real Unigram vocab fixture must parse as a tokenizer")
}
#[test]
fn loads_real_trained_unigram_vocab() {
let tok = load_real_fixture();
assert_eq!(tok.vocab_size(), 100);
}
#[test]
fn matches_real_sentencepiece_reference_encodings() {
let tok = load_real_fixture();
let cases: &[(&str, &[u32])] = &[
("hello world", &[3, 63, 4, 95, 8, 3, 36, 14, 11]),
(
"The quick brown fox",
&[34, 3, 89, 10, 65, 70, 57, 49, 73, 12, 54, 8, 30],
),
(
"Testing unicode: café",
&[74, 44, 20, 35, 47, 4, 83, 3, 62, 13, 25, 18],
),
(
"Numbers 12345",
&[3, 86, 50, 15, 53, 5, 3, 75, 76, 77, 81, 82],
),
("a", &[58]),
(
" multiple spaces ",
&[55, 10, 14, 64, 16, 99, 22, 3, 5, 99, 13, 27, 5],
),
(
"Zurich naive resume",
&[3, 88, 10, 7, 16, 51, 38, 16, 33, 60, 4, 5, 50, 4],
),
(
"punctuation! test? yes.",
&[24, 10, 72, 43, 29, 80, 3, 64, 44, 84, 3, 28, 4, 5, 6],
),
(
"unknown_gibberish_xyz_qqq_zzz",
&[
3, 10, 12, 70, 12, 8, 73, 12, 0, 17, 16, 15, 15, 53, 56, 63, 0, 30, 28, 90, 0,
89, 89, 89, 0, 90, 90, 90,
],
),
];
for (text, expected) in cases {
let got = tok.encode(text);
assert_eq!(&got, expected, "text={text:?}");
}
}
#[test]
fn decode_reverses_encode_for_ascii_text() {
let tok = load_real_fixture();
let ids = tok.encode("hello world");
assert_eq!(tok.decode(&ids), " hello world");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_round_trips_exactly() {
let text = "hello ferrox";
let ids = ByteTokenizer::encode(text);
assert_eq!(ids.len(), text.len());
assert_eq!(ByteTokenizer::decode(&ids), text);
}
#[test]
fn utf8_multibyte_round_trips_exactly() {
let text = "caffe\u{300} \u{1f980}"; let ids = ByteTokenizer::encode(text);
assert_eq!(ByteTokenizer::decode(&ids), text);
}
#[test]
fn all_ids_are_within_byte_vocab_range() {
let ids = ByteTokenizer::encode("mixed ASCII and \u{00e9}\u{00e8} text");
assert!(ids
.iter()
.all(|&id| (id as usize) < ByteTokenizer::VOCAB_SIZE));
}
#[test]
fn empty_string_round_trips() {
assert_eq!(ByteTokenizer::encode(""), Vec::<u32>::new());
assert_eq!(ByteTokenizer::decode(&[]), "");
}
#[test]
fn out_of_range_ids_are_dropped_not_corrupting() {
let decoded = ByteTokenizer::decode(&[104, 105, 300, 33]); assert_eq!(decoded, "hi!");
}
}
#[cfg(test)]
mod eog_tests {
use super::*;
use ferrox_gguf::{GgufValue, TensorInfo, TensorSource};
use std::collections::HashMap;
struct MetaOnly(HashMap<String, GgufValue>);
impl TensorSource for MetaOnly {
fn metadata(&self, key: &str) -> Option<&GgufValue> {
self.0.get(key)
}
fn find_tensor(&self, _name: &str) -> Option<&TensorInfo> {
None
}
fn tensor_bytes(&self, name: &str) -> Result<&[u8], ferrox_gguf::GgufError> {
Err(ferrox_gguf::GgufError::TensorNotFound(name.to_string()))
}
fn tensor_mapped_range(
&self,
name: &str,
) -> Result<
(
std::sync::Arc<ferrox_gguf::MmapHandle>,
std::ops::Range<usize>,
),
ferrox_gguf::GgufError,
> {
Err(ferrox_gguf::GgufError::TensorNotFound(name.to_string()))
}
}
fn source(tokens: &[&str], kv: &[(&str, u64)]) -> MetaOnly {
let mut m = HashMap::new();
m.insert(
"tokenizer.ggml.tokens".to_string(),
GgufValue::Array(
tokens
.iter()
.map(|t| GgufValue::String((*t).to_string()))
.collect(),
),
);
for (k, v) in kv {
m.insert((*k).to_string(), GgufValue::U32(*v as u32));
}
MetaOnly(m)
}
#[test]
fn turn_enders_count_even_when_they_are_not_the_metadata_eos() {
let src = source(
&["hello", "<|end_of_text|>", "<|eot_id|>", "world"],
&[("tokenizer.ggml.eos_token_id", 1)],
);
let eog = eog_token_ids(&src);
assert!(eog.contains(&1), "metadata eos");
assert!(eog.contains(&2), "<|eot_id|> ends the turn");
assert!(
!eog.contains(&0) && !eog.contains(&3),
"ordinary tokens are not EOG"
);
}
#[test]
fn gemma_style_turn_and_eos_are_both_end_of_generation() {
let src = source(&["<eos>", "<turn|>", "x"], &[]);
let eog = eog_token_ids(&src);
assert!(eog.contains(&0) && eog.contains(&1));
assert!(!eog.contains(&2));
}
#[test]
fn eot_and_eom_metadata_ids_are_included() {
let src = source(
&["a", "b", "c"],
&[
("tokenizer.ggml.eot_token_id", 1),
("tokenizer.ggml.eom_token_id", 2),
],
);
let eog = eog_token_ids(&src);
assert!(eog.contains(&1) && eog.contains(&2));
}
#[test]
fn a_file_with_nothing_to_go_on_yields_no_stop_tokens() {
let src = source(&["a", "b"], &[]);
assert!(eog_token_ids(&src).is_empty());
}
}