1use llama_cpp_2::model::LlamaModel;
16use llama_cpp_2::sampling::LlamaSampler;
17use llama_cpp_2::token::LlamaToken;
18
19use super::{NormalizeError, Session, decode_piece};
20
21pub const NGRAM: usize = 2;
23
24pub const K: usize = 64;
27
28pub fn draft<'a>(
34 source: &'a [LlamaToken],
35 generated: &[LlamaToken],
36 ngram: usize,
37 k: usize,
38) -> &'a [LlamaToken] {
39 if ngram == 0 || generated.len() < ngram || source.len() <= ngram {
40 return &[];
41 }
42 let tail = &generated[generated.len() - ngram..];
43 for start in (0..=source.len() - ngram).rev() {
44 if &source[start..start + ngram] == tail {
45 let from = start + ngram;
46 let to = (from + k).min(source.len());
47 if from < to {
48 return &source[from..to];
49 }
50 }
51 }
52 &[]
53}
54
55pub fn generate(
60 model: &LlamaModel,
61 session: &mut Session<'_>,
62 source: &[LlamaToken],
63 ngram: usize,
64 k: usize,
65) -> Result<String, NormalizeError> {
66 let Session { ctx, batch, start, budget } = session;
67 let (start, budget) = (*start, *budget);
68 let mut sampler = LlamaSampler::greedy();
69 let vocab = model.vocab();
70 let mut decoder = encoding_rs::UTF_8.new_decoder();
71 let mut out = String::new();
72 let mut emitted: Vec<LlamaToken> = Vec::new();
73 let mut pos = start;
74 let mut current = sampler.sample(ctx, batch.n_tokens() - 1);
75
76 while emitted.len() < budget {
77 if vocab.is_eog(current) {
78 break;
79 }
80 sampler.accept(current);
81 out.push_str(&decode_piece(&mut decoder, &vocab.token_to_piece(current, false, None)));
82 emitted.push(current);
83
84 let guess = draft(source, &emitted, ngram, k);
85
86 batch.clear();
89 batch.add(current, pos, &[0], true)?;
90 for (i, token) in guess.iter().enumerate() {
91 let offset = i32::try_from(i).unwrap_or(i32::MAX);
92 batch.add(*token, pos + 1 + offset, &[0], true)?;
93 }
94 ctx.decode(batch)?;
95
96 let mut taken = 0usize;
98 let mut next = sampler.sample(ctx, 0);
99 while taken < guess.len()
104 && next == guess[taken]
105 && !vocab.is_eog(next)
106 && emitted.len() < budget
107 {
108 sampler.accept(next);
109 out.push_str(&decode_piece(&mut decoder, &vocab.token_to_piece(next, false, None)));
110 emitted.push(next);
111 taken += 1;
112 next = sampler.sample(ctx, i32::try_from(taken).unwrap_or(i32::MAX));
113 }
114
115 if taken < guess.len() {
119 let first_stale = u32::try_from(pos + 1)
120 .unwrap_or(u32::MAX)
121 .saturating_add(u32::try_from(taken).unwrap_or(u32::MAX));
122 ctx.kv_cache_seq_rm(0, Some(first_stale), None)?;
123 }
124
125 pos += 1 + i32::try_from(taken).unwrap_or(i32::MAX);
126 current = next;
127 }
128
129 if emitted.len() >= budget {
132 tracing::debug!(
133 event_name = "normalization_token_budget_reached",
134 budget,
135 "normalization hit its token ceiling; output is truncated"
136 );
137 }
138
139 Ok(out)
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 fn tokens(ids: &[i32]) -> Vec<LlamaToken> {
147 ids.iter().copied().map(LlamaToken).collect()
148 }
149
150 #[test]
153 fn drafts_from_the_most_recent_match() {
154 let source = tokens(&[1, 2, 99, 1, 2, 42, 43]);
155 let generated = tokens(&[7, 1, 2]);
156 assert_eq!(
157 draft(&source, &generated, 2, 4),
158 tokens(&[42, 43]).as_slice(),
159 "draft did not continue from the last occurrence of the tail"
160 );
161 }
162
163 #[test]
164 fn draft_is_capped_at_k() {
165 let source = tokens(&[1, 2, 3, 4, 5, 6]);
166 let generated = tokens(&[1, 2]);
167 assert_eq!(
168 draft(&source, &generated, 2, 2),
169 tokens(&[3, 4]).as_slice(),
170 "draft exceeded k tokens"
171 );
172 }
173
174 #[test]
177 fn no_draft_without_a_match() {
178 let source = tokens(&[1, 2, 3]);
179 assert!(
180 draft(&source, &tokens(&[8, 9]), 2, 4).is_empty(),
181 "drafted from a tail that does not occur in the source"
182 );
183 assert!(
184 draft(&source, &tokens(&[1]), 2, 4).is_empty(),
185 "drafted from fewer generated tokens than the ngram width"
186 );
187 }
188
189 #[test]
198 fn zero_turns_speculation_off() {
199 let source = tokens(&[1, 2, 3, 4, 5]);
200 let generated = tokens(&[1, 2]);
201 let by_ngram = draft(&source, &generated, 0, 4);
202 assert!(by_ngram.is_empty(), "ngram = 0 drafted {by_ngram:?}");
203 let by_k = draft(&source, &generated, 2, 0);
204 assert!(by_k.is_empty(), "k = 0 drafted {by_k:?}");
205 }
206}