Skip to main content

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}