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").field("len", &self.len()).finish()
101 }
102}
103
104unsafe impl Send for NgramCache {}
107
108impl Drop for NgramCache {
109 fn drop(&mut self) {
110 unsafe { sys::common_shim_ngram_cache_free(self.raw.as_ptr()) }
111 }
112}
113
114impl Default for NgramCache {
115 fn default() -> Self {
116 Self::new()
117 }
118}
119
120impl NgramCache {
121 #[must_use]
127 pub fn new() -> Self {
128 let raw = unsafe { sys::common_shim_ngram_cache_init() };
129 Self {
130 raw: NonNull::new(raw).expect("common_shim_ngram_cache_init returned null"),
131 }
132 }
133
134 pub fn load(path: &str) -> Result<Self> {
141 let c_path = CString::new(path)?;
142 let raw = unsafe { sys::common_shim_ngram_cache_load(c_path.as_ptr()) };
143 NonNull::new(raw)
144 .map(|raw| Self { raw })
145 .ok_or_else(|| NgramError::Failed(last_error()))
146 }
147
148 pub fn save(&mut self, path: &str) -> Result<()> {
155 let c_path = CString::new(path)?;
156 let status =
157 unsafe { sys::common_shim_ngram_cache_save(self.raw.as_ptr(), c_path.as_ptr()) };
158 check_status(status)
159 }
160
161 pub fn merge(&mut self, other: &mut NgramCache) -> Result<()> {
167 let status =
168 unsafe { sys::common_shim_ngram_cache_merge(self.raw.as_ptr(), other.raw.as_ptr()) };
169 check_status(status)
170 }
171
172 #[must_use]
174 pub fn len(&self) -> usize {
175 unsafe { sys::common_shim_ngram_cache_size(self.raw.as_ptr()) }
176 }
177
178 #[must_use]
180 pub fn is_empty(&self) -> bool {
181 self.len() == 0
182 }
183
184 pub fn update(
195 &mut self,
196 ngram_min: i32,
197 ngram_max: i32,
198 tokens: &[LlamaToken],
199 nnew: i32,
200 print_progress: bool,
201 ) -> Result<()> {
202 let raw: Vec<i32> = tokens.iter().map(|t| t.0).collect();
203 let status = unsafe {
204 sys::common_shim_ngram_cache_update(
205 self.raw.as_ptr(),
206 ngram_min,
207 ngram_max,
208 raw.as_ptr(),
209 raw.len(),
210 nnew,
211 print_progress,
212 )
213 };
214 check_status(status)
215 }
216}
217
218pub fn ngram_cache_draft(
228 tokens: &[LlamaToken],
229 n_draft: i32,
230 ngram_min: i32,
231 ngram_max: i32,
232 context: Option<&mut NgramCache>,
233 dynamic: Option<&mut NgramCache>,
234 statik: Option<&mut NgramCache>,
235) -> Result<Vec<LlamaToken>> {
236 if tokens.is_empty() {
237 return Err(NgramError::InvalidArg);
238 }
239 let raw: Vec<i32> = tokens.iter().map(|t| t.0).collect();
240 let ctx_ptr = context.map_or(std::ptr::null_mut(), |c| c.raw.as_ptr());
241 let dyn_ptr = dynamic.map_or(std::ptr::null_mut(), |c| c.raw.as_ptr());
242 let sta_ptr = statik.map_or(std::ptr::null_mut(), |c| c.raw.as_ptr());
243
244 read_tokens(|out, cap, len| unsafe {
245 sys::common_shim_ngram_cache_draft(
246 raw.as_ptr(),
247 raw.len(),
248 n_draft,
249 ngram_min,
250 ngram_max,
251 ctx_ptr,
252 dyn_ptr,
253 sta_ptr,
254 out,
255 cap,
256 len,
257 )
258 })
259}
260
261pub struct NgramMap {
273 raw: NonNull<sys::common_shim_ngram_map>,
274}
275
276impl std::fmt::Debug for NgramMap {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 f.debug_struct("NgramMap").finish_non_exhaustive()
279 }
280}
281
282unsafe impl Send for NgramMap {}
284
285impl Drop for NgramMap {
286 fn drop(&mut self) {
287 unsafe { sys::common_shim_ngram_map_free(self.raw.as_ptr()) }
288 }
289}
290
291impl NgramMap {
292 pub fn new(size_key: u16, size_value: u16, key_only: bool, min_hits: u16) -> Result<Self> {
305 let raw =
306 unsafe { sys::common_shim_ngram_map_init(size_key, size_value, key_only, min_hits) };
307 NonNull::new(raw)
308 .map(|raw| Self { raw })
309 .ok_or_else(|| NgramError::Failed(last_error()))
310 }
311
312 pub fn begin(&mut self, tokens: &[LlamaToken]) -> Result<()> {
318 let raw: Vec<i32> = tokens.iter().map(|t| t.0).collect();
319 let status = unsafe {
320 sys::common_shim_ngram_map_begin(self.raw.as_ptr(), raw.as_ptr(), raw.len())
321 };
322 check_status(status)
323 }
324
325 pub fn draft(&mut self, tokens: &[LlamaToken], sampled: LlamaToken) -> Result<Vec<LlamaToken>> {
334 let raw: Vec<i32> = tokens.iter().map(|t| t.0).collect();
335 read_tokens(|out, cap, len| unsafe {
336 sys::common_shim_ngram_map_draft(
337 self.raw.as_ptr(),
338 raw.as_ptr(),
339 raw.len(),
340 sampled.0,
341 out,
342 cap,
343 len,
344 )
345 })
346 }
347
348 pub fn accept(&mut self, n_accepted: u16) {
353 unsafe { sys::common_shim_ngram_map_accept(self.raw.as_ptr(), n_accepted) }
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 fn toks(v: &[i32]) -> Vec<LlamaToken> {
362 v.iter().copied().map(LlamaToken).collect()
363 }
364
365 #[test]
372 fn simple_draft_predicts_a_repeat() {
373 let history = toks(&[9, 1, 2, 3, 4, 5, 1]);
374 let draft = ngram_simple_draft(2, 2, &history, LlamaToken(2)).unwrap();
375 assert_eq!(
376 draft,
377 toks(&[3, 4]),
378 "expected the continuation of the earlier `1 2`"
379 );
380 }
381
382 #[test]
385 fn simple_draft_with_sampled_already_in_history_finds_nothing() {
386 let history = toks(&[9, 1, 2, 3, 4, 5, 1, 2]);
387 let draft = ngram_simple_draft(2, 2, &history, LlamaToken(2)).unwrap();
388 assert!(draft.is_empty(), "got {draft:?}");
389 }
390
391 #[test]
394 fn simple_draft_is_empty_below_the_length_floor() {
395 let history = toks(&[1, 2, 1, 2, 1]);
396 assert!(ngram_simple_draft(2, 2, &history, LlamaToken(2))
397 .unwrap()
398 .is_empty());
399 }
400
401 #[test]
404 fn simple_draft_is_empty_without_a_repeat() {
405 let history = toks(&[1, 2, 3, 4, 5]);
406 let draft = ngram_simple_draft(2, 2, &history, LlamaToken(5)).unwrap();
407 assert!(draft.is_empty(), "expected no draft, got {draft:?}");
408 }
409
410 #[test]
411 fn simple_draft_handles_empty_history() {
412 assert!(ngram_simple_draft(2, 2, &[], LlamaToken(1)).unwrap().is_empty());
413 }
414
415 #[test]
416 fn cache_starts_empty_and_learns() {
417 let mut cache = NgramCache::new();
418 assert!(cache.is_empty());
419
420 let tokens = toks(&[1, 2, 3, 1, 2, 3, 1, 2, 3]);
421 cache
422 .update(1, 4, &tokens, i32::try_from(tokens.len()).unwrap(), false)
423 .unwrap();
424 assert!(!cache.is_empty(), "update recorded nothing");
425 }
426
427 #[test]
430 fn cache_round_trips_through_disk() {
431 let dir = std::env::temp_dir();
432 let path = dir.join("llama_cpp_rs_ngram_test.bin");
433 let path_str = path.to_str().unwrap();
434
435 let mut cache = NgramCache::new();
436 let tokens = toks(&[7, 8, 9, 7, 8, 9, 7, 8, 9]);
437 cache
438 .update(1, 4, &tokens, i32::try_from(tokens.len()).unwrap(), false)
439 .unwrap();
440 let saved_len = cache.len();
441 cache.save(path_str).unwrap();
442
443 let loaded = NgramCache::load(path_str).unwrap();
444 assert_eq!(loaded.len(), saved_len, "cache changed size across disk");
445
446 let _ = std::fs::remove_file(&path);
447 }
448
449 #[test]
450 fn cache_load_rejects_a_missing_file() {
451 assert!(NgramCache::load("/definitely/not/a/cache.bin").is_err());
452 }
453
454 #[test]
455 fn cache_rejects_interior_nul_in_path() {
456 let mut cache = NgramCache::new();
457 assert!(matches!(cache.save("a\0b"), Err(NgramError::Nul(_))));
458 assert!(matches!(NgramCache::load("a\0b"), Err(NgramError::Nul(_))));
459 }
460
461 #[test]
464 fn cache_merge_is_additive() {
465 let tokens = toks(&[4, 5, 6, 4, 5, 6, 4, 5, 6]);
466 let mut a = NgramCache::new();
467 a.update(1, 4, &tokens, i32::try_from(tokens.len()).unwrap(), false).unwrap();
468 let before = a.len();
469
470 let mut b = NgramCache::new();
471 b.update(1, 4, &tokens, i32::try_from(tokens.len()).unwrap(), false).unwrap();
472
473 a.merge(&mut b).unwrap();
474 assert!(a.len() >= before, "merge lost entries");
475 }
476
477 #[test]
478 fn cache_draft_requires_tokens() {
479 assert!(matches!(
480 ngram_cache_draft(&[], 4, 1, 4, None, None, None),
481 Err(NgramError::InvalidArg)
482 ));
483 }
484
485 #[test]
488 fn cache_draft_with_no_caches_is_empty() {
489 let tokens = toks(&[1, 2, 3]);
490 let draft = ngram_cache_draft(&tokens, 4, 1, 4, None, None, None).unwrap();
491 assert!(draft.is_empty(), "got {draft:?}");
492 }
493
494 #[test]
495 fn cache_draft_predicts_a_learned_repeat() {
496 let tokens = toks(&[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2]);
497 let mut cache = NgramCache::new();
498 cache
499 .update(1, 4, &tokens, i32::try_from(tokens.len()).unwrap(), false)
500 .unwrap();
501
502 let draft = ngram_cache_draft(&tokens, 4, 1, 4, Some(&mut cache), None, None).unwrap();
503 assert!(
504 draft.contains(&LlamaToken(3)),
505 "expected 3 after 1,2; got {draft:?}"
506 );
507 }
508
509 #[test]
510 fn map_builds_and_drafts() {
511 let mut map = NgramMap::new(2, 2, false, 1).expect("map");
512 let tokens = toks(&[1, 2, 3, 4, 1, 2]);
513 map.begin(&tokens).unwrap();
514 let _ = map.draft(&tokens, LlamaToken(2)).unwrap();
517 map.accept(0);
518 }
519
520 #[test]
521 fn map_begin_accepts_an_empty_prompt() {
522 let mut map = NgramMap::new(2, 2, false, 1).expect("map");
523 map.begin(&[]).unwrap();
524 }
525}