use std::{collections::BTreeMap, fs, path::Path};
use crate::{Error, Result};
const BPE_MAGIC: &str = "oa_bpe_v1";
const BASE_VOCAB_SIZE: usize = 256;
const MAX_PERSISTED_MERGES: usize = 1_000_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BpeMerge {
pub left: u32,
pub right: u32,
}
pub struct BpeTokenizer {
target_vocab: usize,
merges: Vec<BpeMerge>,
}
impl BpeTokenizer {
pub fn new(target_vocab: usize) -> Self {
Self {
target_vocab: target_vocab.clamp(BASE_VOCAB_SIZE, u32::MAX as usize),
merges: Vec::new(),
}
}
pub fn train(&mut self, bytes: &[u8], num_merges: usize) {
self.merges.clear();
let mut ids = bytes
.iter()
.map(|byte| u32::from(*byte))
.collect::<Vec<_>>();
let limit = num_merges.min(self.target_vocab - BASE_VOCAB_SIZE);
for _ in 0..limit {
if ids.len() < 2 {
break;
}
let mut counts = BTreeMap::<(u32, u32), usize>::new();
for pair in ids.windows(2) {
*counts.entry((pair[0], pair[1])).or_default() += 1;
}
let best = counts.into_iter().filter(|(_, count)| *count >= 2).max_by(
|(left_pair, left_count), (right_pair, right_count)| {
left_count
.cmp(right_count)
.then_with(|| right_pair.cmp(left_pair))
},
);
let Some(((left, right), _)) = best else {
break;
};
let merge = BpeMerge { left, right };
let new_token = u32::try_from(BASE_VOCAB_SIZE + self.merges.len())
.expect("BPE target vocabulary cannot exceed usize before u32");
self.merges.push(merge);
ids = apply_merge(&ids, merge, new_token);
}
}
pub fn train_text(&mut self, text: &str, num_merges: usize) {
self.train(text.as_bytes(), num_merges);
}
pub fn encode(&self, bytes: &[u8]) -> Vec<u32> {
let mut tokens = bytes
.iter()
.map(|byte| u32::from(*byte))
.collect::<Vec<_>>();
for (rank, merge) in self.merges.iter().copied().enumerate() {
let new_token =
u32::try_from(BASE_VOCAB_SIZE + rank).expect("validated BPE rank must fit u32");
tokens = apply_merge(&tokens, merge, new_token);
}
tokens
}
pub fn encode_text(&self, text: &str) -> Vec<u32> {
self.encode(text.as_bytes())
}
pub fn decode(&self, tokens: &[u32]) -> Result<Vec<u8>> {
let mut output = Vec::new();
let mut pending = Vec::new();
pending
.try_reserve(tokens.len())
.map_err(|_| Error::resource_exhausted("BPE decode stack allocation failed"))?;
pending.extend(tokens.iter().rev().copied());
while let Some(token) = pending.pop() {
if token < BASE_VOCAB_SIZE as u32 {
output
.try_reserve(1)
.map_err(|_| Error::resource_exhausted("BPE decoded output allocation failed"))?;
output.push(token as u8);
continue;
}
let Ok(rank) = usize::try_from(token - BASE_VOCAB_SIZE as u32) else {
continue;
};
let Some(merge) = self.merges.get(rank) else {
continue;
};
pending
.try_reserve(2)
.map_err(|_| Error::resource_exhausted("BPE decode stack allocation failed"))?;
pending.push(merge.right);
pending.push(merge.left);
}
Ok(output)
}
pub fn decode_text(&self, tokens: &[u32]) -> Result<String> {
String::from_utf8(self.decode(tokens)?)
.map_err(|_| Error::data_loss("BPE tokens do not decode to valid UTF-8"))
}
pub fn encode_prompt(&self, prompt: &[u8], context_length: usize) -> Vec<u32> {
let tokens = self.encode(prompt);
let copied = tokens.len().min(context_length);
let mut output = vec![0_u32; context_length];
output[context_length - copied..].copy_from_slice(&tokens[tokens.len() - copied..]);
output
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
let mut text = format!("{BPE_MAGIC}\n{}\n", self.merges.len());
for merge in &self.merges {
text.push_str(&format!("{} {}\n", merge.left, merge.right));
}
fs::write(path, text).map_err(|source| Error::io("BPE vocabulary write", source))
}
pub fn load(&mut self, path: impl AsRef<Path>) -> Result<()> {
let bytes = fs::read(path).map_err(|source| Error::io("BPE vocabulary read", source))?;
let text = std::str::from_utf8(&bytes)
.map_err(|_| Error::data_loss("BPE vocabulary is not UTF-8 text"))?;
let mut fields = text.split_ascii_whitespace();
if fields.next() != Some(BPE_MAGIC) {
return Err(Error::data_loss("BPE vocabulary has an invalid header"));
}
let count = parse_usize(fields.next(), "merge count")?;
if count > MAX_PERSISTED_MERGES {
return Err(Error::data_loss("BPE merge count exceeds the format limit"));
}
let mut merges = Vec::with_capacity(count);
for rank in 0..count {
let left = parse_u32(fields.next(), "left merge token")?;
let right = parse_u32(fields.next(), "right merge token")?;
let next_token = u32::try_from(BASE_VOCAB_SIZE + rank)
.map_err(|_| Error::data_loss("BPE token rank exceeds u32"))?;
if left >= next_token || right >= next_token {
return Err(Error::data_loss(
"BPE merge references its own or a later token",
));
}
merges.push(BpeMerge { left, right });
}
if fields.next().is_some() {
return Err(Error::data_loss("BPE vocabulary has trailing fields"));
}
self.target_vocab = BASE_VOCAB_SIZE + merges.len();
self.merges = merges;
Ok(())
}
pub const fn vocab_size(&self) -> usize {
BASE_VOCAB_SIZE + self.merges.len()
}
pub const fn num_merges(&self) -> usize {
self.merges.len()
}
pub fn merges(&self) -> &[BpeMerge] {
&self.merges
}
pub fn token_byte_len(&self, token: u32) -> Result<usize> {
let mut length = 0_usize;
let mut pending = vec![token];
while let Some(token) = pending.pop() {
if token < BASE_VOCAB_SIZE as u32 {
length = length
.checked_add(1)
.ok_or_else(|| Error::resource_exhausted("BPE token byte length overflows usize"))?;
continue;
}
let Ok(rank) = usize::try_from(token - BASE_VOCAB_SIZE as u32) else {
continue;
};
let Some(merge) = self.merges.get(rank) else {
continue;
};
pending
.try_reserve(2)
.map_err(|_| Error::resource_exhausted("BPE token traversal allocation failed"))?;
pending.push(merge.right);
pending.push(merge.left);
}
Ok(length)
}
}
impl Default for BpeTokenizer {
fn default() -> Self {
Self::new(512)
}
}
fn apply_merge(ids: &[u32], merge: BpeMerge, new_token: u32) -> Vec<u32> {
let mut output = Vec::with_capacity(ids.len());
let mut index = 0;
while index < ids.len() {
if index + 1 < ids.len() && ids[index] == merge.left && ids[index + 1] == merge.right {
output.push(new_token);
index += 2;
} else {
output.push(ids[index]);
index += 1;
}
}
output
}
fn parse_usize(field: Option<&str>, label: &str) -> Result<usize> {
field
.and_then(|field| field.parse().ok())
.ok_or_else(|| Error::data_loss(format!("BPE vocabulary has an invalid {label}")))
}
fn parse_u32(field: Option<&str>, label: &str) -> Result<u32> {
field
.and_then(|field| field.parse().ok())
.ok_or_else(|| Error::data_loss(format!("BPE vocabulary has an invalid {label}")))
}