Skip to main content

llama_cpp_4/
llama_backend.rs

1//! Representation of an initialized llama backend
2
3use crate::LLamaCppError;
4use llama_cpp_sys_4::ggml_log_level;
5use std::sync::atomic::AtomicBool;
6use std::sync::atomic::Ordering::SeqCst;
7
8/// Representation of an initialized llama backend
9/// This is required as a parameter for most llama functions as the backend must be initialized
10/// before any llama functions are called. This type is proof of initialization.
11#[derive(Eq, PartialEq, Debug)]
12pub struct LlamaBackend {}
13
14static LLAMA_BACKEND_INITIALIZED: AtomicBool = AtomicBool::new(false);
15
16impl LlamaBackend {
17    /// Mark the llama backend as initialized
18    fn mark_init() -> crate::Result<()> {
19        match LLAMA_BACKEND_INITIALIZED.compare_exchange(false, true, SeqCst, SeqCst) {
20            Ok(_) => Ok(()),
21            Err(_) => Err(LLamaCppError::BackendAlreadyInitialized),
22        }
23    }
24
25    /// Initialize the llama backend (without numa).
26    ///
27    /// # Examples
28    ///
29    /// ```
30    ///# use llama_cpp_4::llama_backend::LlamaBackend;
31    ///# use llama_cpp_4::LLamaCppError;
32    ///# use std::error::Error;
33    ///
34    ///# fn main() -> Result<(), Box<dyn Error>> {
35    ///
36    ///
37    /// let backend = LlamaBackend::init()?;
38    /// // the llama backend can only be initialized once
39    /// assert_eq!(Err(LLamaCppError::BackendAlreadyInitialized), LlamaBackend::init());
40    ///
41    ///# Ok(())
42    ///# }
43    /// ```
44    /// # Errors
45    ///
46    /// Returns [`LLamaCppError::BackendAlreadyInitialized`] if the backend has already been initialized.
47    #[tracing::instrument(skip_all)]
48    pub fn init() -> crate::Result<LlamaBackend> {
49        Self::mark_init()?;
50        // SAFETY: `llama_backend_init` must be called exactly once per process
51        // before any other llama.cpp entry point. `mark_init` is the gate: it
52        // compare-exchanges a global from false to true and has already
53        // returned `Err` above if anyone got here first, so this cannot run
54        // twice even from several threads.
55        unsafe { llama_cpp_sys_4::llama_backend_init() }
56        Ok(LlamaBackend {})
57    }
58
59    /// Initialize the llama backend (with numa).
60    /// ```
61    ///# use llama_cpp_4::llama_backend::LlamaBackend;
62    ///# use std::error::Error;
63    ///# use llama_cpp_4::llama_backend::NumaStrategy;
64    ///
65    ///# fn main() -> Result<(), Box<dyn Error>> {
66    ///
67    /// let llama_backend = LlamaBackend::init_numa(NumaStrategy::MIRROR)?;
68    ///
69    ///# Ok(())
70    ///# }
71    /// ```
72    /// # Errors
73    ///
74    /// Returns [`LLamaCppError::BackendAlreadyInitialized`] if the backend has already been initialized.
75    #[tracing::instrument(skip_all)]
76    pub fn init_numa(strategy: NumaStrategy) -> crate::Result<LlamaBackend> {
77        Self::mark_init()?;
78        // SAFETY: same once-only gate as `init`. The argument is a
79        // `ggml_numa_strategy` built by the `From` impl below, so it is always
80        // one of the discriminants llama.cpp matches on.
81        unsafe {
82            llama_cpp_sys_4::llama_numa_init(llama_cpp_sys_4::ggml_numa_strategy::from(strategy));
83        }
84        Ok(LlamaBackend {})
85    }
86
87    /// Change the output of llama.cpp's logging to be voided instead of pushed to `stderr`.
88    pub fn void_logs(&mut self) {
89        unsafe extern "C" fn void_log(
90            _level: ggml_log_level,
91            _text: *const ::std::os::raw::c_char,
92            _user_data: *mut ::std::os::raw::c_void,
93        ) {
94        }
95
96        // SAFETY: `void_log` is a `'static` fn item with the exact signature
97        // `ggml_log_callback` requires, and it touches none of its arguments —
98        // so it is sound for llama.cpp to call from any thread, at any time,
99        // for the rest of the process. The null user-data pointer is never
100        // dereferenced because `void_log` ignores it. `&mut self` is what keeps
101        // this from racing another `void_log`/`llama_log_set` caller.
102        unsafe {
103            llama_cpp_sys_4::llama_log_set(Some(void_log), std::ptr::null_mut());
104        }
105    }
106}
107
108/// A rusty wrapper around `numa_strategy`.
109///
110/// ## Description
111/// Represents different NUMA (Non-Uniform Memory Access) strategies for memory management
112/// in multi-core or multi-processor systems.
113///
114/// ## See more
115/// <https://github.com/ggerganov/llama.cpp/blob/master/ggml/include/ggml-cpu.h#L25-L32>
116#[derive(Debug, Eq, PartialEq, Copy, Clone)]
117pub enum NumaStrategy {
118    /// The NUMA strategy is disabled. No NUMA-aware optimizations are applied.
119    /// Memory allocation will not consider NUMA node locality.
120    DISABLED,
121
122    /// Distribute memory across NUMA nodes. This strategy aims to balance memory usage
123    /// across all available NUMA nodes, potentially improving load balancing and preventing
124    /// memory hotspots on a single node. It may use round-robin or another method to
125    /// distribute allocations.
126    DISTRIBUTE,
127
128    /// Isolate memory to specific NUMA nodes. Memory allocations will be restricted to
129    /// specific NUMA nodes, potentially reducing contention and improving locality for
130    /// processes or threads bound to a particular node.
131    ISOLATE,
132
133    /// Use `numactl` to manage memory and processor affinities. This strategy utilizes
134    /// the `numactl` command or library to bind processes or memory allocations to specific
135    /// NUMA nodes or CPUs, providing fine-grained control over memory placement.
136    NUMACTL,
137
138    /// Mirror memory across NUMA nodes. This strategy creates duplicate memory copies
139    /// on multiple NUMA nodes, which can help with fault tolerance and redundancy,
140    /// ensuring that each NUMA node has access to a copy of the memory.
141    MIRROR,
142
143    /// A placeholder representing the total number of strategies available.
144    /// Typically used for iteration or determining the number of strategies in the enum.
145    COUNT,
146}
147
148/// An invalid numa strategy was provided.
149#[derive(Debug, Eq, PartialEq, Copy, Clone)]
150pub struct InvalidNumaStrategy(
151    /// The invalid numa strategy that was provided.
152    pub llama_cpp_sys_4::ggml_numa_strategy,
153);
154
155impl TryFrom<llama_cpp_sys_4::ggml_numa_strategy> for NumaStrategy {
156    type Error = InvalidNumaStrategy;
157
158    fn try_from(value: llama_cpp_sys_4::ggml_numa_strategy) -> Result<Self, Self::Error> {
159        match value {
160            llama_cpp_sys_4::GGML_NUMA_STRATEGY_DISABLED => Ok(Self::DISABLED),
161            llama_cpp_sys_4::GGML_NUMA_STRATEGY_DISTRIBUTE => Ok(Self::DISTRIBUTE),
162            llama_cpp_sys_4::GGML_NUMA_STRATEGY_ISOLATE => Ok(Self::ISOLATE),
163            llama_cpp_sys_4::GGML_NUMA_STRATEGY_NUMACTL => Ok(Self::NUMACTL),
164            llama_cpp_sys_4::GGML_NUMA_STRATEGY_MIRROR => Ok(Self::MIRROR),
165            llama_cpp_sys_4::GGML_NUMA_STRATEGY_COUNT => Ok(Self::COUNT),
166            value => Err(InvalidNumaStrategy(value)),
167        }
168    }
169}
170
171impl From<NumaStrategy> for llama_cpp_sys_4::ggml_numa_strategy {
172    fn from(value: NumaStrategy) -> Self {
173        match value {
174            NumaStrategy::DISABLED => llama_cpp_sys_4::GGML_NUMA_STRATEGY_DISABLED,
175            NumaStrategy::DISTRIBUTE => llama_cpp_sys_4::GGML_NUMA_STRATEGY_DISTRIBUTE,
176            NumaStrategy::ISOLATE => llama_cpp_sys_4::GGML_NUMA_STRATEGY_ISOLATE,
177            NumaStrategy::NUMACTL => llama_cpp_sys_4::GGML_NUMA_STRATEGY_NUMACTL,
178            NumaStrategy::MIRROR => llama_cpp_sys_4::GGML_NUMA_STRATEGY_MIRROR,
179            NumaStrategy::COUNT => llama_cpp_sys_4::GGML_NUMA_STRATEGY_COUNT,
180        }
181    }
182}
183
184/// Drops the llama backend.
185/// ```
186///
187///# use llama_cpp_4::llama_backend::LlamaBackend;
188///# use std::error::Error;
189///
190///# fn main() -> Result<(), Box<dyn Error>> {
191/// let backend = LlamaBackend::init()?;
192/// drop(backend);
193/// // can be initialized again after being dropped
194/// let backend = LlamaBackend::init()?;
195///# Ok(())
196///# }
197///
198/// ```
199impl Drop for LlamaBackend {
200    fn drop(&mut self) {
201        match LLAMA_BACKEND_INITIALIZED.compare_exchange(true, false, SeqCst, SeqCst) {
202            Ok(_) => {}
203            Err(_) => {
204                unreachable!("This should not be reachable as the only ways to obtain a llama backend involve marking the backend as initialized.")
205            }
206        }
207        // SAFETY: paired with the `llama_backend_init` in `init`/`init_numa`.
208        // Reaching `Drop` means a `LlamaBackend` existed, which means the
209        // backend was initialized, and the compare-exchange just above flipped
210        // the global back to false — so this runs once and nothing else can be
211        // mid-call.
212        unsafe { llama_cpp_sys_4::llama_backend_free() }
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn numa_from_and_to() {
222        let numas = [
223            NumaStrategy::DISABLED,
224            NumaStrategy::DISTRIBUTE,
225            NumaStrategy::ISOLATE,
226            NumaStrategy::NUMACTL,
227            NumaStrategy::MIRROR,
228            NumaStrategy::COUNT,
229        ];
230
231        for numa in &numas {
232            let from = llama_cpp_sys_4::ggml_numa_strategy::from(*numa);
233            let to = NumaStrategy::try_from(from).expect("Failed to convert from and to");
234            assert_eq!(*numa, to);
235        }
236    }
237
238    #[test]
239    fn check_invalid_numa() {
240        let invalid = 800;
241        let invalid = NumaStrategy::try_from(invalid);
242        assert_eq!(invalid, Err(InvalidNumaStrategy(invalid.unwrap_err().0)));
243    }
244}