ik_llama_cpp_2/model/lora.rs
1//! LoRA adapters (`llama_lora_adapter_*`).
2//!
3//! Mirrors `llama-cpp-2`'s LoRA API ([`crate::model::LlamaModel::lora_adapter_init`]
4//! and the `LlamaContext::lora_adapter_*` methods), adapted for ik_llama.cpp's
5//! older C API: ik exposes per-adapter `llama_lora_adapter_set` / `_remove` /
6//! `_clear` over an `llama_lora_adapter` handle, whereas modern stock llama.cpp
7//! replaced these with `llama_set_adapters_lora` over an `llama_adapter_lora`
8//! type.
9
10use std::ffi::{CString, NulError};
11use std::path::{Path, PathBuf};
12use std::ptr::NonNull;
13
14use ik_llama_cpp_sys as sys;
15
16/// A safe wrapper around `llama_lora_adapter`.
17///
18/// The adapter is initialized from a GGUF file via
19/// [`crate::model::LlamaModel::lora_adapter_init`].
20///
21/// # Ownership
22///
23/// In ik_llama.cpp the adapter is **owned by the model**: it registers itself
24/// with the model on init and is freed automatically when the model is freed
25/// (`llama.h`: "will be freed when the model is deleted"). This type therefore
26/// has **no `Drop`** — freeing it here would double-free (once here, once by the
27/// model). Keep the [`crate::model::LlamaModel`] it came from alive for at least
28/// as long as this adapter, and do not use an adapter after `lora_adapter_clear`
29/// or after the owning model is dropped. (This mirrors the `llama-cpp-2` anchor.)
30#[derive(Debug)]
31#[repr(transparent)]
32#[allow(clippy::module_name_repetitions)]
33pub struct LlamaLoraAdapter {
34 pub(crate) lora_adapter: NonNull<sys::llama_lora_adapter>,
35}
36
37/// An error that can occur when initializing a lora adapter.
38#[derive(Debug, Eq, PartialEq, thiserror::Error)]
39pub enum LlamaLoraAdapterInitError {
40 /// There was a null byte in a provided string and thus it could not be converted to a C string.
41 #[error("null byte in string {0}")]
42 NullError(#[from] NulError),
43 /// llama.cpp returned a nullptr - this could be many different causes.
44 #[error("null result from llama cpp")]
45 NullResult,
46 /// Failed to convert the path to a rust str. This means the path was not valid unicode.
47 #[error("failed to convert path {0} to str")]
48 PathToStrError(PathBuf),
49}
50
51/// An error that can occur when setting a lora adapter on a context.
52#[derive(Debug, Eq, PartialEq, thiserror::Error)]
53pub enum LlamaLoraAdapterSetError {
54 /// llama.cpp returned a non-zero error code.
55 #[error("error code from llama cpp")]
56 ErrorResult(i32),
57}
58
59/// An error that can occur when removing a lora adapter from a context.
60#[derive(Debug, Eq, PartialEq, thiserror::Error)]
61pub enum LlamaLoraAdapterRemoveError {
62 /// llama.cpp returned a non-zero error code.
63 #[error("error code from llama cpp")]
64 ErrorResult(i32),
65}
66
67impl crate::model::LlamaModel {
68 /// Initializes a lora adapter from a file.
69 ///
70 /// # Errors
71 ///
72 /// See [`LlamaLoraAdapterInitError`] for more information.
73 pub fn lora_adapter_init(
74 &self,
75 path: &Path,
76 ) -> Result<LlamaLoraAdapter, LlamaLoraAdapterInitError> {
77 debug_assert!(path.exists(), "{path:?} does not exist");
78
79 let path_str = path
80 .to_str()
81 .ok_or_else(|| LlamaLoraAdapterInitError::PathToStrError(path.to_path_buf()))?;
82
83 let cstr = CString::new(path_str)?;
84 // SAFETY: `self.model` is a valid, non-null model pointer and `cstr` is a
85 // valid NUL-terminated C string that outlives the call.
86 let adapter = unsafe { sys::llama_lora_adapter_init(self.model.as_ptr(), cstr.as_ptr()) };
87
88 let adapter = NonNull::new(adapter).ok_or(LlamaLoraAdapterInitError::NullResult)?;
89
90 tracing::debug!(?path, "Initialized lora adapter");
91 Ok(LlamaLoraAdapter {
92 lora_adapter: adapter,
93 })
94 }
95}
96
97// NOTE: intentionally NO `impl Drop` — the model owns and frees the adapter
98// (see the `LlamaLoraAdapter` "Ownership" docs). A `Drop` calling
99// `llama_lora_adapter_free` here would double-free (the model frees it too) and
100// could UAF if the adapter were still set on a live context.
101
102impl crate::context::LlamaContext<'_> {
103 /// Sets a lora adapter on this context with the given scale.
104 ///
105 /// # Errors
106 ///
107 /// See [`LlamaLoraAdapterSetError`] for more information.
108 pub fn lora_adapter_set(
109 &mut self,
110 adapter: &LlamaLoraAdapter,
111 scale: f32,
112 ) -> Result<(), LlamaLoraAdapterSetError> {
113 // SAFETY: both the context and adapter pointers are valid and non-null
114 // for the duration of the call.
115 let err_code = unsafe {
116 sys::llama_lora_adapter_set(self.context.as_ptr(), adapter.lora_adapter.as_ptr(), scale)
117 };
118 if err_code != 0 {
119 return Err(LlamaLoraAdapterSetError::ErrorResult(err_code));
120 }
121
122 tracing::debug!(scale, "Set lora adapter");
123 Ok(())
124 }
125
126 /// Removes a specific lora adapter from this context.
127 ///
128 /// # Errors
129 ///
130 /// See [`LlamaLoraAdapterRemoveError`] for more information.
131 pub fn lora_adapter_remove(
132 &mut self,
133 adapter: &LlamaLoraAdapter,
134 ) -> Result<(), LlamaLoraAdapterRemoveError> {
135 // SAFETY: both the context and adapter pointers are valid and non-null
136 // for the duration of the call.
137 let err_code = unsafe {
138 sys::llama_lora_adapter_remove(self.context.as_ptr(), adapter.lora_adapter.as_ptr())
139 };
140 if err_code != 0 {
141 return Err(LlamaLoraAdapterRemoveError::ErrorResult(err_code));
142 }
143
144 tracing::debug!("Removed lora adapter");
145 Ok(())
146 }
147
148 /// Removes all lora adapters from this context.
149 pub fn lora_adapter_clear(&mut self) {
150 // SAFETY: `self.context` is a valid, non-null context pointer.
151 unsafe { sys::llama_lora_adapter_clear(self.context.as_ptr()) }
152 tracing::debug!("Cleared lora adapters");
153 }
154}