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