Skip to main content

llama_cpp_4/
fit.rs

1//! Memory estimation and parameter fitting from llama.cpp `common/fit`.
2//!
3//! - [`get_device_memory_data`] — project per-device memory for a parameter set
4//!   without keeping a context alive.
5//! - [`fit_params`] — adjust [`LlamaModelParams`] / [`LlamaContextParams`] to fit
6//!   available device memory (upstream `common_fit_params`).
7//!
8//! # Example — memory estimate
9//!
10//! ```no_run
11//! use llama_cpp_4::prelude::*;
12//! use std::path::Path;
13//!
14//! fn main() {
15//!     let _backend = LlamaBackend::init().unwrap();
16//!     let report = get_device_memory_data(
17//!         Path::new("model.gguf"),
18//!         &LlamaModelParams::default().with_n_gpu_layers(99),
19//!         &LlamaContextParams::default(),
20//!         llama_cpp_sys_4::GGML_LOG_LEVEL_ERROR,
21//!     )
22//!     .unwrap();
23//!
24//!     println!("training ctx: {}", report.hyperparams.n_ctx_train);
25//!     for (i, entry) in report.entries.iter().enumerate() {
26//!         println!(
27//!             "device {i}: {} bytes free / {} total (projected {})",
28//!             entry.free,
29//!             entry.total,
30//!             entry.used(),
31//!         );
32//!     }
33//! }
34//! ```
35//!
36//! # Example — auto-fit parameters
37//!
38//! ```no_run
39//! use llama_cpp_4::fit::{fit_params, FitParams};
40//! use llama_cpp_4::prelude::*;
41//! use std::path::Path;
42//!
43//! fn main() {
44//!     let backend = LlamaBackend::init().unwrap();
45//!     let result = fit_params(
46//!         &backend,
47//!         Path::new("model.gguf"),
48//!         FitParams::default().with_n_ctx_min(512),
49//!     )
50//!     .unwrap();
51//!
52//!     use std::num::NonZeroU32;
53//!
54//!     println!("n_ctx: {}", result.context_params.n_ctx().map_or(0, NonZeroU32::get));
55//!     println!("n_gpu_layers: {}", result.model_params.n_gpu_layers());
56//! }
57//! ```
58
59use std::ffi::CString;
60use std::path::Path;
61use std::ptr::{null, null_mut};
62
63use thiserror::Error;
64
65use crate::context::params::LlamaContextParams;
66use crate::llama_backend::LlamaBackend;
67use crate::model::params::LlamaModelParams;
68use crate::{max_devices, max_tensor_buft_overrides};
69
70/// Per-device memory projection from [`get_device_memory_data`].
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct DeviceMemoryEntry {
73    /// Total device memory in bytes.
74    pub total: i64,
75    /// Free device memory in bytes at query time.
76    pub free: i64,
77    /// Projected model weight bytes on this device.
78    pub model: usize,
79    /// Projected KV / recurrent cache bytes.
80    pub context: usize,
81    /// Projected temporary compute buffer bytes.
82    pub compute: usize,
83}
84
85impl DeviceMemoryEntry {
86    /// Sum of model, context, and compute bytes.
87    #[must_use]
88    pub fn used(&self) -> usize {
89        self.model + self.context + self.compute
90    }
91}
92
93/// Hyper-parameters discovered while estimating device memory.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct DeviceMemoryHyperParams {
96    /// Model `n_gpu_layers` hyper-parameter used for the estimate.
97    pub n_gpu_layers: u32,
98    /// Model training context length.
99    pub n_ctx_train: u32,
100    /// Number of `MoE` experts (`0` when dense).
101    pub n_expert: u32,
102}
103
104/// Result of [`get_device_memory_data`].
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct DeviceMemoryReport {
107    /// Per-device memory breakdown (one entry per backend device).
108    pub entries: Vec<DeviceMemoryEntry>,
109    /// Hyper-parameters read from the checkpoint during estimation.
110    pub hyperparams: DeviceMemoryHyperParams,
111}
112
113/// Errors from [`get_device_memory_data`].
114#[derive(Debug, Error, PartialEq, Eq)]
115pub enum DeviceMemoryError {
116    /// The model path could not be encoded as a C string.
117    #[error("invalid model path")]
118    InvalidPath,
119    /// The underlying C++ helper failed (model missing, incompatible params, …).
120    #[error("device memory query failed")]
121    QueryFailed,
122    /// More devices were reported than the internal buffer allows.
123    #[error("device memory entry buffer overflow")]
124    BufferOverflow,
125}
126
127/// Estimate per-device memory for a model path and parameter set.
128///
129/// This wraps `common_get_device_memory_data` through `ext_shim`. The model is
130/// loaded with `no_alloc` and freed before returning; no context is kept alive.
131///
132/// # Errors
133///
134/// Returns [`DeviceMemoryError`] when the path is invalid or llama.cpp cannot
135/// produce an estimate.
136pub fn get_device_memory_data(
137    path_model: &Path,
138    mparams: &LlamaModelParams,
139    cparams: &LlamaContextParams,
140    log_level: llama_cpp_sys_4::ggml_log_level,
141) -> Result<DeviceMemoryReport, DeviceMemoryError> {
142    let path = CString::new(path_model.to_string_lossy().as_ref())
143        .map_err(|_| DeviceMemoryError::InvalidPath)?;
144
145    let mparams = mparams.params;
146    let cparams = cparams.context_params;
147
148    let mut capacity = 8usize;
149    loop {
150        let mut raw = vec![
151            llama_cpp_sys_4::common_device_memory_flat_entry {
152                total: 0,
153                free: 0,
154                model: 0,
155                context: 0,
156                compute: 0,
157            };
158            capacity
159        ];
160        let mut hp_ngl = 0u32;
161        let mut hp_nct = 0u32;
162        let mut hp_nex = 0u32;
163
164        // SAFETY: the arguments are either scalars or pointers that outlive the call, and
165        // the library owns whatever it returns.
166        let n = unsafe {
167            llama_cpp_sys_4::common_device_memory_collect(
168                path.as_ptr(),
169                &raw const mparams,
170                &raw const cparams,
171                log_level,
172                raw.as_mut_ptr(),
173                capacity,
174                &raw mut hp_ngl,
175                &raw mut hp_nct,
176                &raw mut hp_nex,
177            )
178        };
179
180        if n == usize::MAX {
181            return Err(DeviceMemoryError::QueryFailed);
182        }
183
184        if n < capacity {
185            let entries = raw
186                .into_iter()
187                .take(n)
188                .map(|e| DeviceMemoryEntry {
189                    total: e.total,
190                    free: e.free,
191                    model: e.model,
192                    context: e.context,
193                    compute: e.compute,
194                })
195                .collect();
196            return Ok(DeviceMemoryReport {
197                entries,
198                hyperparams: DeviceMemoryHyperParams {
199                    n_gpu_layers: hp_ngl,
200                    n_ctx_train: hp_nct,
201                    n_expert: hp_nex,
202                },
203            });
204        }
205
206        capacity = capacity.saturating_mul(2);
207        if capacity > 256 {
208            return Err(DeviceMemoryError::BufferOverflow);
209        }
210    }
211}
212
213const DEFAULT_MARGIN_BYTES: usize = 1024 * 1024 * 1024;
214
215/// Input to [`fit_params`].
216///
217/// Defaults mirror upstream `common_params`: unset `n_ctx` (`0`) so context size
218/// can be reduced, default model params so `n_gpu_layers` may be adjusted, and
219/// 1 GiB per-device memory margins.
220#[derive(Debug)]
221pub struct FitParams {
222    /// Starting model parameters. Only fields still at their defaults are modified.
223    pub model_params: LlamaModelParams,
224    /// Starting context parameters. Set `n_ctx` to `0` via
225    /// [`LlamaContextParams::with_n_ctx`]`(None)` to let fitting pick a context size.
226    pub context_params: LlamaContextParams,
227    /// Minimum free memory to leave on each device, in bytes (one entry per device).
228    pub margins: Vec<usize>,
229    /// Minimum context size when fitting must reduce `n_ctx`.
230    pub n_ctx_min: u32,
231    /// Minimum log level printed during fitting.
232    pub log_level: llama_cpp_sys_4::ggml_log_level,
233}
234
235impl Default for FitParams {
236    fn default() -> Self {
237        let nd = max_devices();
238        Self {
239            model_params: LlamaModelParams::default(),
240            context_params: LlamaContextParams::default().with_n_ctx(None),
241            margins: vec![DEFAULT_MARGIN_BYTES; nd],
242            n_ctx_min: 4096,
243            log_level: llama_cpp_sys_4::GGML_LOG_LEVEL_ERROR,
244        }
245    }
246}
247
248impl FitParams {
249    /// Override starting model parameters.
250    #[must_use]
251    pub fn with_model_params(mut self, model_params: LlamaModelParams) -> Self {
252        self.model_params = model_params;
253        self
254    }
255
256    /// Override starting context parameters.
257    #[must_use]
258    pub fn with_context_params(mut self, context_params: LlamaContextParams) -> Self {
259        self.context_params = context_params;
260        self
261    }
262
263    /// Per-device memory margins in bytes (length must be at least [`max_devices`]`()`).
264    #[must_use]
265    pub fn with_margins(mut self, margins: Vec<usize>) -> Self {
266        self.margins = margins;
267        self
268    }
269
270    /// Minimum context size when fitting reduces memory by shrinking `n_ctx`.
271    #[must_use]
272    pub fn with_n_ctx_min(mut self, n_ctx_min: u32) -> Self {
273        self.n_ctx_min = n_ctx_min;
274        self
275    }
276
277    /// Minimum log level emitted while fitting.
278    #[must_use]
279    pub fn with_log_level(mut self, log_level: llama_cpp_sys_4::ggml_log_level) -> Self {
280        self.log_level = log_level;
281        self
282    }
283}
284
285/// Fitted model/context parameters plus auxiliary buffers.
286///
287/// [`LlamaModelParams`] inside this struct may point at [`Self::tensor_split`] and
288/// its internal tensor buffer-type override storage; keep the whole `FitParamsResult` alive while
289/// loading a model with these parameters.
290#[derive(Debug)]
291pub struct FitParamsResult {
292    /// Model parameters after fitting (`n_gpu_layers`, tensor split, …).
293    pub model_params: LlamaModelParams,
294    /// Context parameters after fitting (`n_ctx`, …).
295    pub context_params: LlamaContextParams,
296    /// Layer split ratios per device (writable buffer passed to llama.cpp).
297    pub tensor_split: Vec<f32>,
298    /// Tensor buffer-type overrides written by fitting (keeps pointers valid).
299    #[allow(dead_code)]
300    pub(crate) tensor_buft_overrides: Vec<llama_cpp_sys_4::llama_model_tensor_buft_override>,
301    /// Per-device memory margins used during fitting (bytes).
302    pub margins: Vec<usize>,
303}
304
305impl FitParamsResult {
306    /// Tensor split values for active devices, trimming trailing zeros.
307    #[must_use]
308    pub fn active_tensor_split(&self) -> &[f32] {
309        let mut nd = self.tensor_split.len();
310        while nd > 1 && self.tensor_split[nd - 1] == 0.0 {
311            nd -= 1;
312        }
313        &self.tensor_split[..nd]
314    }
315}
316
317/// Errors from [`fit_params`].
318#[derive(Debug, Error, PartialEq, Eq)]
319pub enum FitParamsError {
320    /// The model path could not be encoded as a C string.
321    #[error("invalid model path")]
322    InvalidPath,
323    /// Fitting could not find allocations that fit device memory.
324    #[error("could not fit parameters to available device memory")]
325    CouldNotFit,
326    /// A hard error occurred (e.g. model file missing).
327    #[error("parameter fitting failed")]
328    Failed,
329}
330
331/// Adjust model and context parameters to fit available device memory.
332///
333/// Wraps `common_fit_params`. Requires an initialized [`LlamaBackend`]. The model
334/// is probed with `no_alloc` internally; nothing is kept loaded on return.
335///
336/// Only model fields still equal to [`LlamaModelParams::default`] are modified
337/// (except `n_gpu_layers` on macOS where the default is `-1`). Context `n_ctx`
338/// is adjusted only when it is `0` — use [`LlamaContextParams::with_n_ctx`] with `None`.
339///
340/// # Errors
341///
342/// Returns [`FitParamsError::InvalidPath`] for bad paths,
343/// [`FitParamsError::CouldNotFit`] when no allocation fits, and
344/// [`FitParamsError::Failed`] on hard errors (missing model, incompatible params, …).
345pub fn fit_params(
346    _backend: &LlamaBackend,
347    path_model: &Path,
348    options: FitParams,
349) -> Result<FitParamsResult, FitParamsError> {
350    let path = CString::new(path_model.to_string_lossy().as_ref())
351        .map_err(|_| FitParamsError::InvalidPath)?;
352
353    let nd = max_devices();
354    let mut tensor_split = vec![0.0_f32; nd];
355
356    let ntbo = max_tensor_buft_overrides();
357    let mut tensor_buft_overrides = vec![
358        llama_cpp_sys_4::llama_model_tensor_buft_override {
359            pattern: null(),
360            buft: null_mut(),
361        };
362        ntbo + 1
363    ];
364
365    let mut margins = options.margins;
366    if margins.len() < nd {
367        margins.resize(nd, DEFAULT_MARGIN_BYTES);
368    }
369
370    let mut model_params = options.model_params;
371    model_params.params.tensor_split = tensor_split.as_mut_ptr();
372    model_params.params.tensor_buft_overrides = tensor_buft_overrides.as_mut_ptr();
373
374    let mut context_params = options.context_params;
375
376    // SAFETY: the `CString` arguments are alive for the duration of the call, and any
377    // out-param is a stack local the shim writes through.
378    let status = unsafe {
379        llama_cpp_sys_4::common_fit_params(
380            path.as_ptr(),
381            &raw mut model_params.params,
382            &raw mut context_params.context_params,
383            tensor_split.as_mut_ptr(),
384            tensor_buft_overrides.as_mut_ptr(),
385            margins.as_mut_ptr(),
386            options.n_ctx_min,
387            // `extra`: a second model to fit alongside the main one (a draft
388            // model, say). This entry point fits one model, so there is none.
389            null(),
390            options.log_level,
391        )
392    };
393
394    match status {
395        llama_cpp_sys_4::COMMON_PARAMS_FIT_STATUS_SUCCESS => {
396            model_params.params.tensor_split = tensor_split.as_mut_ptr();
397            model_params.params.tensor_buft_overrides = tensor_buft_overrides.as_mut_ptr();
398            Ok(FitParamsResult {
399                model_params,
400                context_params,
401                tensor_split,
402                tensor_buft_overrides,
403                margins,
404            })
405        }
406        llama_cpp_sys_4::COMMON_PARAMS_FIT_STATUS_FAILURE => Err(FitParamsError::CouldNotFit),
407        _ => Err(FitParamsError::Failed),
408    }
409}