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}