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