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}