Skip to main content

llama_cpp_4/model/
params.rs

1//! A safe wrapper around `llama_model_params`.
2
3use crate::model::params::kv_overrides::KvOverrides;
4use std::ffi::{c_char, CStr};
5use std::fmt::{Debug, Formatter};
6use std::pin::Pin;
7use std::ptr::null;
8
9pub mod kv_overrides;
10
11/// Exact model-file loading strategy exposed by llama.cpp.
12///
13/// `llama_load_mode` is a signed enum on every target because of the negative
14/// `LLAMA_LOAD_MODE_AUTO` discriminant, so each variant uses `as _` to coerce to
15/// the `#[repr(i32)]` type (matching [`token_type`](crate::token_type)).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[repr(i32)]
18pub enum LlamaLoadMode {
19    /// Pick the strategy from the backend devices' capabilities: memory-map when
20    /// every device supports it, otherwise fall back to a plain read. This is
21    /// llama.cpp's default.
22    Auto = llama_cpp_sys_4::LLAMA_LOAD_MODE_AUTO as _,
23    /// No memory mapping, locking, or direct I/O.
24    None = llama_cpp_sys_4::LLAMA_LOAD_MODE_NONE as _,
25    /// Memory-map model files when supported.
26    Mmap = llama_cpp_sys_4::LLAMA_LOAD_MODE_MMAP as _,
27    /// Read model files normally and lock loaded pages in memory.
28    Mlock = llama_cpp_sys_4::LLAMA_LOAD_MODE_MLOCK as _,
29    /// Memory-map model files and lock mapped pages in memory.
30    MmapMlock = llama_cpp_sys_4::LLAMA_LOAD_MODE_MMAP_MLOCK as _,
31    /// Use direct I/O when supported.
32    DirectIo = llama_cpp_sys_4::LLAMA_LOAD_MODE_DIRECT_IO as _,
33}
34
35impl LlamaLoadMode {
36    /// llama.cpp's own name for this mode: `"auto"`, `"none"`, `"mmap"`,
37    /// `"mlock"`, `"mmap+mlock"` or `"dio"`.
38    ///
39    /// Wraps `llama_load_mode_name`, so the spelling always matches what
40    /// upstream's logs print and what its `--load-mode` flag accepts.
41    ///
42    /// # Panics
43    ///
44    /// Panics if llama.cpp returns a non-UTF-8 name, which would mean the
45    /// upstream table was corrupted.
46    #[must_use]
47    pub fn name(self) -> &'static str {
48        // SAFETY: the argument is a plain enum discriminant and upstream
49        // returns a pointer to a string literal; the null case is asserted
50        // rather than assumed.
51        let ptr = unsafe { llama_cpp_sys_4::llama_load_mode_name(self as _) };
52        assert!(!ptr.is_null(), "llama_load_mode_name returned null");
53        // SAFETY: upstream returns a pointer to a string literal, so `'static`
54        // holds.
55        unsafe { CStr::from_ptr(ptr) }
56            .to_str()
57            .expect("llama_load_mode_name returned non-UTF-8")
58    }
59
60    /// Parse a mode from llama.cpp's own spelling — the inverse of
61    /// [`Self::name`]. Returns `None` if `name` matches no mode.
62    ///
63    /// ```
64    /// # use llama_cpp_4::model::params::LlamaLoadMode;
65    /// assert_eq!(LlamaLoadMode::from_name("mmap+mlock"), Some(LlamaLoadMode::MmapMlock));
66    /// assert_eq!(LlamaLoadMode::from_name("nonsense"), None);
67    /// ```
68    //
69    // Deliberately *not* a call to `llama_load_mode_from_str`: that function
70    // throws `std::invalid_argument` for an unrecognised string, and letting a
71    // C++ exception unwind across the `extern "C"` boundary into Rust is
72    // undefined behaviour. Comparing against `name()` uses upstream's own
73    // strings, so this cannot drift from the C table it mirrors — the
74    // round-trip test pins that.
75    #[must_use]
76    pub fn from_name(name: &str) -> Option<Self> {
77        [
78            Self::Auto,
79            Self::None,
80            Self::Mmap,
81            Self::Mlock,
82            Self::MmapMlock,
83            Self::DirectIo,
84        ]
85        .into_iter()
86        .find(|mode| mode.name() == name)
87    }
88}
89
90/// Whether tensors the architecture marks as lazy are read on demand rather
91/// than up front.
92///
93/// Only tensors the model architecture flags carry this at all — today that is
94/// Gemma-4's per-layer token embedding and `Qwen4Exp`'s PLE rows — so on every
95/// other architecture the setting has no effect. Lazy reading always needs
96/// mmap; without it llama.cpp warns and loads the tensor in full regardless.
97///
98/// `llama_lazy_mode` is an unsigned enum upstream (all discriminants are
99/// non-negative), unlike the signed [`LlamaLoadMode`].
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101#[repr(u32)]
102pub enum LlamaLazyMode {
103    /// Never read lazily — always pull the whole tensor up front.
104    Off = llama_cpp_sys_4::LLAMA_LAZY_MODE_OFF as _,
105    /// Read lazily only for marked tensors larger than 4 GiB. llama.cpp's
106    /// default, and downgraded to [`LlamaLazyMode::Off`] at load time if any
107    /// backend device lacks mmap support (iGPUs, for instance).
108    Auto = llama_cpp_sys_4::LLAMA_LAZY_MODE_AUTO as _,
109    /// Read every marked tensor's rows on demand, whatever its size. Trades
110    /// I/O for resident memory; the 4 GiB floor exists because the per-read
111    /// overhead is not worth it on small tensors.
112    On = llama_cpp_sys_4::LLAMA_LAZY_MODE_ON as _,
113}
114
115/// A safe wrapper around `llama_model_params`.
116#[allow(clippy::module_name_repetitions)]
117pub struct LlamaModelParams {
118    pub(crate) params: llama_cpp_sys_4::llama_model_params,
119    kv_overrides: Vec<llama_cpp_sys_4::llama_model_kv_override>,
120}
121
122impl Debug for LlamaModelParams {
123    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
124        f.debug_struct("LlamaModelParams")
125            .field("n_gpu_layers", &self.params.n_gpu_layers)
126            .field("main_gpu", &self.params.main_gpu)
127            .field("vocab_only", &self.params.vocab_only)
128            .field("load_mode", &self.load_mode())
129            .field("lazy_mode", &self.lazy_mode())
130            .field("load_mtp", &self.load_mtp())
131            .field("kv_overrides", &"vec of kv_overrides")
132            .finish()
133    }
134}
135
136impl LlamaModelParams {
137    /// See [`KvOverrides`]
138    ///
139    /// # Examples
140    ///
141    /// ```rust
142    /// # use llama_cpp_4::model::params::LlamaModelParams;
143    /// let params = Box::pin(LlamaModelParams::default());
144    /// let kv_overrides = params.kv_overrides();
145    /// let count = kv_overrides.into_iter().count();
146    /// assert_eq!(count, 0);
147    /// ```
148    #[must_use]
149    pub fn kv_overrides(&self) -> KvOverrides<'_> {
150        KvOverrides::new(self)
151    }
152
153    /// Appends a key-value override to the model parameters. It must be pinned as this creates a self-referential struct.
154    ///
155    /// # Examples
156    ///
157    /// ```rust
158    /// # use std::ffi::{CStr, CString};
159    /// use std::pin::pin;
160    /// # use llama_cpp_4::model::params::LlamaModelParams;
161    /// # use llama_cpp_4::model::params::kv_overrides::ParamOverrideValue;
162    /// let mut params = pin!(LlamaModelParams::default());
163    /// let key = CString::new("key").expect("CString::new failed");
164    /// params.as_mut().append_kv_override(&key, ParamOverrideValue::Int(50));
165    ///
166    /// let kv_overrides = params.kv_overrides().into_iter().collect::<Vec<_>>();
167    /// assert_eq!(kv_overrides.len(), 1);
168    ///
169    /// let (k, v) = &kv_overrides[0];
170    /// assert_eq!(v, &ParamOverrideValue::Int(50));
171    ///
172    /// assert_eq!(k.to_bytes(), b"key", "expected key to be 'key', was {:?}", k);
173    /// ```
174    #[allow(clippy::missing_panics_doc)] // panics are just to enforce internal invariants, not user errors
175    pub fn append_kv_override(
176        mut self: Pin<&mut Self>,
177        key: &CStr,
178        value: kv_overrides::ParamOverrideValue,
179    ) {
180        let kv_override = self
181            .kv_overrides
182            .get_mut(0)
183            .expect("kv_overrides did not have a next allocated");
184
185        assert_eq!(kv_override.key[0], 0, "last kv_override was not empty");
186
187        // There should be some way to do this without iterating over everything.
188        for (i, &c) in key.to_bytes_with_nul().iter().enumerate() {
189            kv_override.key[i] = c_char::try_from(c).expect("invalid character in key");
190        }
191
192        kv_override.tag = value.tag();
193        kv_override.__bindgen_anon_1 = value.value();
194
195        // set to null pointer for panic safety (as push may move the vector, invalidating the pointer)
196        self.params.kv_overrides = null();
197
198        // push the next one to ensure we maintain the iterator invariant of ending with a 0
199        self.kv_overrides
200            .push(llama_cpp_sys_4::llama_model_kv_override {
201                key: [0; 128],
202                tag: 0,
203                __bindgen_anon_1: llama_cpp_sys_4::llama_model_kv_override__bindgen_ty_1 {
204                    val_i64: 0,
205                },
206            });
207
208        // set the pointer to the (potentially) new vector
209        self.params.kv_overrides = self.kv_overrides.as_ptr();
210
211        eprintln!("saved ptr: {:?}", self.params.kv_overrides);
212    }
213}
214
215impl LlamaModelParams {
216    /// Get the number of layers to offload to the GPU.
217    #[must_use]
218    pub fn n_gpu_layers(&self) -> i32 {
219        self.params.n_gpu_layers
220    }
221
222    /// The GPU that is used for scratch and small tensors
223    #[must_use]
224    pub fn main_gpu(&self) -> i32 {
225        self.params.main_gpu
226    }
227
228    /// only load the vocabulary, no weights
229    #[must_use]
230    pub fn vocab_only(&self) -> bool {
231        self.params.vocab_only
232    }
233
234    /// Returns the exact model-file loading strategy.
235    #[must_use]
236    pub fn load_mode(&self) -> LlamaLoadMode {
237        match self.params.load_mode {
238            llama_cpp_sys_4::LLAMA_LOAD_MODE_AUTO => LlamaLoadMode::Auto,
239            llama_cpp_sys_4::LLAMA_LOAD_MODE_MMAP => LlamaLoadMode::Mmap,
240            llama_cpp_sys_4::LLAMA_LOAD_MODE_MLOCK => LlamaLoadMode::Mlock,
241            llama_cpp_sys_4::LLAMA_LOAD_MODE_MMAP_MLOCK => LlamaLoadMode::MmapMlock,
242            llama_cpp_sys_4::LLAMA_LOAD_MODE_DIRECT_IO => LlamaLoadMode::DirectIo,
243            _ => LlamaLoadMode::None,
244        }
245    }
246
247    /// Returns whether arch-marked tensors are read on demand.
248    ///
249    /// This is the requested mode, not the effective one: llama.cpp resolves
250    /// [`LlamaLazyMode::Auto`] down to [`LlamaLazyMode::Off`] during load when a
251    /// device lacks mmap support, and that resolution is not written back here.
252    #[must_use]
253    pub fn lazy_mode(&self) -> LlamaLazyMode {
254        match self.params.lazy_mode {
255            llama_cpp_sys_4::LLAMA_LAZY_MODE_OFF => LlamaLazyMode::Off,
256            llama_cpp_sys_4::LLAMA_LAZY_MODE_ON => LlamaLazyMode::On,
257            _ => LlamaLazyMode::Auto,
258        }
259    }
260
261    /// Whether the model's MTP (multi-token prediction) layers will be loaded.
262    ///
263    /// MTP layers drive multi-token-prediction speculative decoding for models
264    /// that ship them (e.g. `DeepSeek V4`). Once loaded, the speculative state is
265    /// captured and restored through [`crate::speculative`]. Defaults to `false`
266    /// because most models carry no MTP weights.
267    #[must_use]
268    pub fn load_mtp(&self) -> bool {
269        self.params.load_mtp
270    }
271
272    /// use mmap if possible
273    ///
274    /// [`LlamaLoadMode::Auto`] counts as "possible": llama.cpp memory-maps under
275    /// `Auto` unless one of the backend devices lacks mmap support, which is only
276    /// known once the model is loaded.
277    #[must_use]
278    pub fn use_mmap(&self) -> bool {
279        matches!(
280            self.load_mode(),
281            LlamaLoadMode::Auto | LlamaLoadMode::Mmap | LlamaLoadMode::MmapMlock
282        )
283    }
284
285    /// force system to keep model in RAM
286    #[must_use]
287    pub fn use_mlock(&self) -> bool {
288        matches!(
289            self.load_mode(),
290            LlamaLoadMode::Mlock | LlamaLoadMode::MmapMlock
291        )
292    }
293
294    /// sets the number of gpu layers to offload to the GPU.
295    /// ```
296    /// # use llama_cpp_4::model::params::LlamaModelParams;
297    /// let params = LlamaModelParams::default();
298    /// let params = params.with_n_gpu_layers(1);
299    /// assert_eq!(params.n_gpu_layers(), 1);
300    /// ```
301    #[must_use]
302    pub fn with_n_gpu_layers(mut self, n_gpu_layers: u32) -> Self {
303        // The only way this conversion can fail is if u32 overflows the i32 - in which case we set
304        // to MAX
305        let n_gpu_layers = i32::try_from(n_gpu_layers).unwrap_or(i32::MAX);
306        self.params.n_gpu_layers = n_gpu_layers;
307        self
308    }
309
310    /// sets the main GPU
311    #[must_use]
312    pub fn with_main_gpu(mut self, main_gpu: i32) -> Self {
313        self.params.main_gpu = main_gpu;
314        self
315    }
316
317    /// sets `vocab_only`
318    #[must_use]
319    pub fn with_vocab_only(mut self, vocab_only: bool) -> Self {
320        self.params.vocab_only = vocab_only;
321        self
322    }
323
324    /// Sets the exact model-file loading strategy.
325    #[must_use]
326    pub fn with_load_mode(mut self, load_mode: LlamaLoadMode) -> Self {
327        self.params.load_mode = load_mode as llama_cpp_sys_4::llama_load_mode;
328        self
329    }
330
331    /// Sets whether arch-marked tensors are read on demand.
332    ///
333    /// Reach for [`LlamaLazyMode::On`] when a Gemma-4 or `Qwen4Exp` model's marked
334    /// tensors will not fit in RAM and the extra I/O is the better trade;
335    /// [`LlamaLazyMode::Off`] pins everything in memory up front. Corresponds to
336    /// `llama_model_params.lazy_mode`, added upstream in llama.cpp PR #27794.
337    ///
338    /// ```
339    /// # use llama_cpp_4::model::params::{LlamaLazyMode, LlamaModelParams};
340    /// let params = LlamaModelParams::default().with_lazy_mode(LlamaLazyMode::On);
341    /// assert_eq!(params.lazy_mode(), LlamaLazyMode::On);
342    /// ```
343    #[must_use]
344    pub fn with_lazy_mode(mut self, lazy_mode: LlamaLazyMode) -> Self {
345        self.params.lazy_mode = lazy_mode as llama_cpp_sys_4::llama_lazy_mode;
346        self
347    }
348
349    /// Sets whether to load the model's MTP (multi-token prediction) layers.
350    ///
351    /// Enable this for models that ship MTP weights (e.g. `DeepSeek V4`) when you
352    /// intend to use MTP-based speculative decoding, then drive the speculative
353    /// state via [`crate::speculative`]. For models without MTP layers the flag
354    /// has no effect. Corresponds to `llama_model_params.load_mtp`, added
355    /// upstream in llama.cpp PR #25784 (`DeepSeek V4` MTP + `DSpark`).
356    ///
357    /// ```
358    /// # use llama_cpp_4::model::params::LlamaModelParams;
359    /// let params = LlamaModelParams::default().with_load_mtp(true);
360    /// assert!(params.load_mtp());
361    /// ```
362    #[must_use]
363    pub fn with_load_mtp(mut self, load_mtp: bool) -> Self {
364        self.params.load_mtp = load_mtp;
365        self
366    }
367
368    /// sets `use_mlock`
369    #[must_use]
370    pub fn with_use_mlock(mut self, use_mlock: bool) -> Self {
371        let load_mode = match (self.use_mmap(), use_mlock) {
372            (true, true) => LlamaLoadMode::MmapMlock,
373            (true, false) => LlamaLoadMode::Mmap,
374            (false, true) => LlamaLoadMode::Mlock,
375            (false, false) => LlamaLoadMode::None,
376        };
377        self.params.load_mode = load_mode as llama_cpp_sys_4::llama_load_mode;
378        self
379    }
380}
381
382/// Default parameters for `LlamaModel`. (as defined in llama.cpp by `llama_model_default_params`)
383/// ```
384/// # use llama_cpp_4::model::params::LlamaModelParams;
385/// let params = LlamaModelParams::default();
386/// assert_eq!(params.n_gpu_layers(), -1, "n_gpu_layers should be -1 (all layers)");
387/// assert_eq!(params.main_gpu(), 0, "main_gpu should be 0");
388/// assert_eq!(params.vocab_only(), false, "vocab_only should be false");
389/// assert_eq!(params.use_mmap(), true, "use_mmap should be true");
390/// assert_eq!(params.use_mlock(), false, "use_mlock should be false");
391/// ```
392impl Default for LlamaModelParams {
393    fn default() -> Self {
394        // SAFETY: no pointer is involved; llama.cpp returns a fully
395        // initialised params struct by value.
396        let default_params = unsafe { llama_cpp_sys_4::llama_model_default_params() };
397        LlamaModelParams {
398            params: default_params,
399            // push the next one to ensure we maintain the iterator invariant of ending with a 0
400            kv_overrides: vec![llama_cpp_sys_4::llama_model_kv_override {
401                key: [0; 128],
402                tag: 0,
403                __bindgen_anon_1: llama_cpp_sys_4::llama_model_kv_override__bindgen_ty_1 {
404                    val_i64: 0,
405                },
406            }],
407        }
408    }
409}