kopitiam_tokenizer/
merges.rs1use kopitiam_core::{Error, Result};
13use std::collections::HashMap;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct MergeRule {
23 pub rank: u32,
24 pub merged_id: u32,
25}
26
27#[derive(Debug, Clone, Default)]
29pub struct MergeTable {
30 rules: HashMap<(u32, u32), MergeRule>,
31}
32
33impl MergeTable {
34 pub fn build(
43 merges: &[(Vec<u8>, Vec<u8>)],
44 mut lookup: impl FnMut(&[u8]) -> Option<u32>,
45 ) -> Result<Self> {
46 let mut rules = HashMap::with_capacity(merges.len());
47 for (rank, (left, right)) in merges.iter().enumerate() {
48 let left_id = lookup(left).ok_or_else(|| Error::MalformedModel {
49 format: "bpe-merges",
50 reason: format!(
51 "merge rule {rank} references left-hand token {left:?}, which is not in the vocab"
52 ),
53 })?;
54 let right_id = lookup(right).ok_or_else(|| Error::MalformedModel {
55 format: "bpe-merges",
56 reason: format!(
57 "merge rule {rank} references right-hand token {right:?}, which is not in the vocab"
58 ),
59 })?;
60 let mut merged = left.clone();
61 merged.extend_from_slice(right);
62 let merged_id = lookup(&merged).ok_or_else(|| Error::MalformedModel {
63 format: "bpe-merges",
64 reason: format!(
65 "merge rule {rank} produces token {merged:?}, which is not in the vocab"
66 ),
67 })?;
68
69 if rules
74 .insert(
75 (left_id, right_id),
76 MergeRule {
77 rank: rank as u32,
78 merged_id,
79 },
80 )
81 .is_some()
82 {
83 return Err(Error::MalformedModel {
84 format: "bpe-merges",
85 reason: format!(
86 "pair ({left_id}, {right_id}) is taught by more than one merge rule"
87 ),
88 });
89 }
90 }
91 Ok(Self { rules })
92 }
93
94 pub fn get(&self, left_id: u32, right_id: u32) -> Option<MergeRule> {
96 self.rules.get(&(left_id, right_id)).copied()
97 }
98
99 pub fn len(&self) -> usize {
100 self.rules.len()
101 }
102
103 pub fn is_empty(&self) -> bool {
104 self.rules.is_empty()
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::vocab::Vocab;
112
113 fn test_vocab() -> Vocab {
114 Vocab::from_entries(vec![
115 b"a".to_vec(),
116 b"b".to_vec(),
117 b"c".to_vec(),
118 b"ab".to_vec(),
119 b"abc".to_vec(),
120 ])
121 .unwrap()
122 }
123
124 #[test]
125 fn resolves_ranks_and_merged_ids() {
126 let vocab = test_vocab();
127 let merges = vec![
128 (b"a".to_vec(), b"b".to_vec()),
129 (b"ab".to_vec(), b"c".to_vec()),
130 ];
131 let table = MergeTable::build(&merges, |b| vocab.id_of(b)).unwrap();
132 assert_eq!(table.len(), 2);
133 let ab = table.get(0, 1).unwrap(); assert_eq!(ab.rank, 0);
135 assert_eq!(ab.merged_id, 3); let abc = table.get(3, 2).unwrap(); assert_eq!(abc.rank, 1);
138 assert_eq!(abc.merged_id, 4); assert!(table.get(0, 2).is_none()); }
141
142 #[test]
143 fn rejects_merge_producing_a_token_outside_the_vocab() {
144 let vocab = Vocab::from_entries(vec![b"a".to_vec(), b"b".to_vec()]).unwrap();
145 let merges = vec![(b"a".to_vec(), b"b".to_vec())]; let err = MergeTable::build(&merges, |b| vocab.id_of(b)).unwrap_err();
147 assert!(matches!(err, Error::MalformedModel { .. }));
148 }
149
150 #[test]
151 fn rejects_duplicate_pair() {
152 let vocab = test_vocab();
153 let merges = vec![
154 (b"a".to_vec(), b"b".to_vec()),
155 (b"a".to_vec(), b"b".to_vec()),
156 ];
157 let err = MergeTable::build(&merges, |b| vocab.id_of(b)).unwrap_err();
158 assert!(matches!(err, Error::MalformedModel { .. }));
159 }
160}