1use std::ffi::CString;
26use std::ptr::NonNull;
27
28use llama_cpp_sys_4 as sys;
29
30use crate::shim::{check_status, last_error, read_tokens, ShimError};
31use crate::token::LlamaToken;
32
33pub type NgramError = ShimError;
35
36type Result<T> = std::result::Result<T, NgramError>;
37
38#[allow(clippy::similar_names)]
59pub fn ngram_simple_draft(
60 size_ngram: u16,
61 size_mgram: u16,
62 tokens: &[LlamaToken],
63 sampled: LlamaToken,
64) -> Result<Vec<LlamaToken>> {
65 let raw: Vec<i32> = tokens.iter().map(|t| t.0).collect();
66 read_tokens(|out, cap, len| unsafe {
67 sys::common_shim_ngram_simple_draft(
68 size_ngram,
69 size_mgram,
70 raw.as_ptr(),
71 raw.len(),
72 sampled.0,
73 out,
74 cap,
75 len,
76 )
77 })
78}
79
80pub struct NgramCache {
95 raw: NonNull<sys::common_shim_ngram_cache>,
96}
97
98impl std::fmt::Debug for NgramCache {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.debug_struct("NgramCache")
101 .field("len", &self.len())
102 .finish()
103 }
104}
105
106unsafe impl Send for NgramCache {}
109
110impl Drop for NgramCache {
111 fn drop(&mut self) {
112 unsafe { sys::common_shim_ngram_cache_free(self.raw.as_ptr()) }
113 }
114}
115
116impl Default for NgramCache {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122impl NgramCache {
123 #[must_use]
129 pub fn new() -> Self {
130 let raw = unsafe { sys::common_shim_ngram_cache_init() };
131 Self {
132 raw: NonNull::new(raw).expect("common_shim_ngram_cache_init returned null"),
133 }
134 }
135
136 pub fn load(path: &str) -> Result<Self> {
143 let c_path = CString::new(path)?;
144 let raw = unsafe { sys::common_shim_ngram_cache_load(c_path.as_ptr()) };
145 NonNull::new(raw)
146 .map(|raw| Self { raw })
147 .ok_or_else(|| NgramError::Failed(last_error()))
148 }
149
150 pub fn save(&mut self, path: &str) -> Result<()> {
157 let c_path = CString::new(path)?;
158 let status =
159 unsafe { sys::common_shim_ngram_cache_save(self.raw.as_ptr(), c_path.as_ptr()) };
160 check_status(status)
161 }
162
163 pub fn merge(&mut self, other: &mut NgramCache) -> Result<()> {
169 let status =
170 unsafe { sys::common_shim_ngram_cache_merge(self.raw.as_ptr(), other.raw.as_ptr()) };
171 check_status(status)
172 }
173
174 #[must_use]
176 pub fn len(&self) -> usize {
177 unsafe { sys::common_shim_ngram_cache_size(self.raw.as_ptr()) }
178 }
179
180 #[must_use]
182 pub fn is_empty(&self) -> bool {
183 self.len() == 0
184 }
185
186 pub fn update(
197 &mut self,
198 ngram_min: i32,
199 ngram_max: i32,
200 tokens: &[LlamaToken],
201 nnew: i32,
202 print_progress: bool,
203 ) -> Result<()> {
204 let raw: Vec<i32> = tokens.iter().map(|t| t.0).collect();
205 let status = unsafe {
206 sys::common_shim_ngram_cache_update(
207 self.raw.as_ptr(),
208 ngram_min,
209 ngram_max,
210 raw.as_ptr(),
211 raw.len(),
212 nnew,
213 print_progress,
214 )
215 };
216 check_status(status)
217 }
218}
219
220pub fn ngram_cache_draft(
230 tokens: &[LlamaToken],
231 n_draft: i32,
232 ngram_min: i32,
233 ngram_max: i32,
234 context: Option<&mut NgramCache>,
235 dynamic: Option<&mut NgramCache>,
236 statik: Option<&mut NgramCache>,
237) -> Result<Vec<LlamaToken>> {
238 if tokens.is_empty() {
239 return Err(NgramError::InvalidArg);
240 }
241 let raw: Vec<i32> = tokens.iter().map(|t| t.0).collect();
242 let ctx_ptr = context.map_or(std::ptr::null_mut(), |c| c.raw.as_ptr());
243 let dyn_ptr = dynamic.map_or(std::ptr::null_mut(), |c| c.raw.as_ptr());
244 let sta_ptr = statik.map_or(std::ptr::null_mut(), |c| c.raw.as_ptr());
245
246 read_tokens(|out, cap, len| unsafe {
247 sys::common_shim_ngram_cache_draft(
248 raw.as_ptr(),
249 raw.len(),
250 n_draft,
251 ngram_min,
252 ngram_max,
253 ctx_ptr,
254 dyn_ptr,
255 sta_ptr,
256 out,
257 cap,
258 len,
259 )
260 })
261}
262
263pub struct NgramMap {
275 raw: NonNull<sys::common_shim_ngram_map>,
276}
277
278impl std::fmt::Debug for NgramMap {
279 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280 f.debug_struct("NgramMap").finish_non_exhaustive()
281 }
282}
283
284unsafe impl Send for NgramMap {}
286
287impl Drop for NgramMap {
288 fn drop(&mut self) {
289 unsafe { sys::common_shim_ngram_map_free(self.raw.as_ptr()) }
290 }
291}
292
293impl NgramMap {
294 pub fn new(size_key: u16, size_value: u16, key_only: bool, min_hits: u16) -> Result<Self> {
307 let raw =
308 unsafe { sys::common_shim_ngram_map_init(size_key, size_value, key_only, min_hits) };
309 NonNull::new(raw)
310 .map(|raw| Self { raw })
311 .ok_or_else(|| NgramError::Failed(last_error()))
312 }
313
314 pub fn begin(&mut self, tokens: &[LlamaToken]) -> Result<()> {
320 let raw: Vec<i32> = tokens.iter().map(|t| t.0).collect();
321 let status =
322 unsafe { sys::common_shim_ngram_map_begin(self.raw.as_ptr(), raw.as_ptr(), raw.len()) };
323 check_status(status)
324 }
325
326 pub fn draft(&mut self, tokens: &[LlamaToken], sampled: LlamaToken) -> Result<Vec<LlamaToken>> {
335 let raw: Vec<i32> = tokens.iter().map(|t| t.0).collect();
336 read_tokens(|out, cap, len| unsafe {
337 sys::common_shim_ngram_map_draft(
338 self.raw.as_ptr(),
339 raw.as_ptr(),
340 raw.len(),
341 sampled.0,
342 out,
343 cap,
344 len,
345 )
346 })
347 }
348
349 pub fn accept(&mut self, n_accepted: u16) {
354 unsafe { sys::common_shim_ngram_map_accept(self.raw.as_ptr(), n_accepted) }
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 fn toks(v: &[i32]) -> Vec<LlamaToken> {
363 v.iter().copied().map(LlamaToken).collect()
364 }
365
366 #[test]
373 fn simple_draft_predicts_a_repeat() {
374 let history = toks(&[9, 1, 2, 3, 4, 5, 1]);
375 let draft = ngram_simple_draft(2, 2, &history, LlamaToken(2)).unwrap();
376 assert_eq!(
377 draft,
378 toks(&[3, 4]),
379 "expected the continuation of the earlier `1 2`"
380 );
381 }
382
383 #[test]
386 fn simple_draft_with_sampled_already_in_history_finds_nothing() {
387 let history = toks(&[9, 1, 2, 3, 4, 5, 1, 2]);
388 let draft = ngram_simple_draft(2, 2, &history, LlamaToken(2)).unwrap();
389 assert!(draft.is_empty(), "got {draft:?}");
390 }
391
392 #[test]
395 fn simple_draft_is_empty_below_the_length_floor() {
396 let history = toks(&[1, 2, 1, 2, 1]);
397 assert!(ngram_simple_draft(2, 2, &history, LlamaToken(2))
398 .unwrap()
399 .is_empty());
400 }
401
402 #[test]
405 fn simple_draft_is_empty_without_a_repeat() {
406 let history = toks(&[1, 2, 3, 4, 5]);
407 let draft = ngram_simple_draft(2, 2, &history, LlamaToken(5)).unwrap();
408 assert!(draft.is_empty(), "expected no draft, got {draft:?}");
409 }
410
411 #[test]
412 fn simple_draft_handles_empty_history() {
413 assert!(ngram_simple_draft(2, 2, &[], LlamaToken(1))
414 .unwrap()
415 .is_empty());
416 }
417
418 #[test]
419 fn cache_starts_empty_and_learns() {
420 let mut cache = NgramCache::new();
421 assert!(cache.is_empty());
422
423 let tokens = toks(&[1, 2, 3, 1, 2, 3, 1, 2, 3]);
424 cache
425 .update(1, 4, &tokens, i32::try_from(tokens.len()).unwrap(), false)
426 .unwrap();
427 assert!(!cache.is_empty(), "update recorded nothing");
428 }
429
430 #[test]
433 fn cache_round_trips_through_disk() {
434 let dir = std::env::temp_dir();
435 let path = dir.join("llama_cpp_rs_ngram_test.bin");
436 let path_str = path.to_str().unwrap();
437
438 let mut cache = NgramCache::new();
439 let tokens = toks(&[7, 8, 9, 7, 8, 9, 7, 8, 9]);
440 cache
441 .update(1, 4, &tokens, i32::try_from(tokens.len()).unwrap(), false)
442 .unwrap();
443 let saved_len = cache.len();
444 cache.save(path_str).unwrap();
445
446 let loaded = NgramCache::load(path_str).unwrap();
447 assert_eq!(loaded.len(), saved_len, "cache changed size across disk");
448
449 let _ = std::fs::remove_file(&path);
450 }
451
452 #[test]
453 fn cache_load_rejects_a_missing_file() {
454 assert!(NgramCache::load("/definitely/not/a/cache.bin").is_err());
455 }
456
457 #[test]
458 fn cache_rejects_interior_nul_in_path() {
459 let mut cache = NgramCache::new();
460 assert!(matches!(cache.save("a\0b"), Err(NgramError::Nul(_))));
461 assert!(matches!(NgramCache::load("a\0b"), Err(NgramError::Nul(_))));
462 }
463
464 #[test]
467 fn cache_merge_is_additive() {
468 let tokens = toks(&[4, 5, 6, 4, 5, 6, 4, 5, 6]);
469 let mut a = NgramCache::new();
470 a.update(1, 4, &tokens, i32::try_from(tokens.len()).unwrap(), false)
471 .unwrap();
472 let before = a.len();
473
474 let mut b = NgramCache::new();
475 b.update(1, 4, &tokens, i32::try_from(tokens.len()).unwrap(), false)
476 .unwrap();
477
478 a.merge(&mut b).unwrap();
479 assert!(a.len() >= before, "merge lost entries");
480 }
481
482 #[test]
483 fn cache_draft_requires_tokens() {
484 assert!(matches!(
485 ngram_cache_draft(&[], 4, 1, 4, None, None, None),
486 Err(NgramError::InvalidArg)
487 ));
488 }
489
490 #[test]
493 fn cache_draft_with_no_caches_is_empty() {
494 let tokens = toks(&[1, 2, 3]);
495 let draft = ngram_cache_draft(&tokens, 4, 1, 4, None, None, None).unwrap();
496 assert!(draft.is_empty(), "got {draft:?}");
497 }
498
499 #[test]
500 fn cache_draft_predicts_a_learned_repeat() {
501 let tokens = toks(&[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2]);
502 let mut cache = NgramCache::new();
503 cache
504 .update(1, 4, &tokens, i32::try_from(tokens.len()).unwrap(), false)
505 .unwrap();
506
507 let draft = ngram_cache_draft(&tokens, 4, 1, 4, Some(&mut cache), None, None).unwrap();
508 assert!(
509 draft.contains(&LlamaToken(3)),
510 "expected 3 after 1,2; got {draft:?}"
511 );
512 }
513
514 #[test]
515 fn map_builds_and_drafts() {
516 let mut map = NgramMap::new(2, 2, false, 1).expect("map");
517 let tokens = toks(&[1, 2, 3, 4, 1, 2]);
518 map.begin(&tokens).unwrap();
519 let _ = map.draft(&tokens, LlamaToken(2)).unwrap();
522 map.accept(0);
523 }
524
525 #[test]
526 fn map_begin_accepts_an_empty_prompt() {
527 let mut map = NgramMap::new(2, 2, false, 1).expect("map");
528 map.begin(&[]).unwrap();
529 }
530}