use std::ffi::{CString, c_char};
use std::path::Path;
use std::ptr::NonNull;
use std::sync::Arc;
use offgrid_sys as sys;
use serde::{Deserialize, Serialize};
use crate::backend::{cstr_to_string, ensure_init, setup_lock};
use crate::media::Projector;
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GpuLayers {
#[default]
All,
None,
Count(u32),
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ModelParams {
pub gpu_layers: GpuLayers,
pub devices: Option<Vec<String>>,
pub mmproj: Option<std::path::PathBuf>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ModelInfo {
pub name: Option<String>,
pub architecture: Option<String>,
pub description: String,
pub n_params: u64,
pub size_bytes: u64,
pub n_ctx_train: u32,
pub n_embd: u32,
pub n_layer: u32,
pub has_encoder: bool,
pub has_decoder: bool,
pub chat_template: Option<String>,
pub vision: bool,
pub audio: bool,
}
pub struct Model {
projector: Option<Projector>,
ptr: NonNull<sys::llama_model>,
}
unsafe impl Send for Model {}
unsafe impl Sync for Model {}
impl Drop for Model {
fn drop(&mut self) {
drop(self.projector.take());
let _guard = setup_lock();
unsafe { sys::llama_model_free(self.ptr.as_ptr()) };
}
}
impl Model {
pub fn load(path: impl AsRef<Path>, params: &ModelParams) -> Result<Arc<Self>> {
ensure_init();
let path = path.as_ref();
if !path.is_file() {
return Err(Error::ModelNotFound(path.to_path_buf()));
}
let c_path = CString::new(path.to_string_lossy().as_bytes())
.map_err(|_| Error::InvalidArgument("model path contains NUL".into()))?;
let mut mp = unsafe { sys::llama_model_default_params() };
mp.n_gpu_layers = match params.gpu_layers {
GpuLayers::All => -1,
GpuLayers::None => 0,
GpuLayers::Count(n) => n.min(i32::MAX as u32) as i32,
};
let mut device_list: Vec<sys::ggml_backend_dev_t> = Vec::new();
if let Some(names) = ¶ms.devices {
for name in names {
device_list.push(find_device(name)?);
}
device_list.push(std::ptr::null_mut());
mp.devices = device_list.as_mut_ptr();
}
let raw = {
let _guard = setup_lock();
unsafe { sys::llama_model_load_from_file(c_path.as_ptr(), mp) }
};
let ptr = NonNull::new(raw).ok_or_else(|| Error::ModelLoad(path.to_path_buf()))?;
let mut model = Self { projector: None, ptr };
if let Some(mmproj) = ¶ms.mmproj {
let threads = crate::chat::default_threads();
model.projector =
Some(Projector::load(mmproj, ptr.as_ptr(), params.gpu_layers != GpuLayers::None, threads)?);
}
Ok(Arc::new(model))
}
pub(crate) fn projector(&self) -> Option<&Projector> {
self.projector.as_ref()
}
pub(crate) fn as_ptr(&self) -> *mut sys::llama_model {
self.ptr.as_ptr()
}
pub(crate) fn vocab(&self) -> *const sys::llama_vocab {
unsafe { sys::llama_model_get_vocab(self.ptr.as_ptr()) }
}
pub fn info(&self) -> ModelInfo {
let m = self.ptr.as_ptr();
let mut desc = vec![0u8; 256];
let n = unsafe { sys::llama_model_desc(m, desc.as_mut_ptr() as *mut c_char, desc.len()) };
desc.truncate(n.max(0) as usize);
unsafe {
ModelInfo {
name: self.meta("general.name"),
architecture: self.meta("general.architecture"),
description: String::from_utf8_lossy(&desc).into_owned(),
n_params: sys::llama_model_n_params(m),
size_bytes: sys::llama_model_size(m),
n_ctx_train: sys::llama_model_n_ctx_train(m).max(0) as u32,
n_embd: sys::llama_model_n_embd(m).max(0) as u32,
n_layer: sys::llama_model_n_layer(m).max(0) as u32,
has_encoder: sys::llama_model_has_encoder(m),
has_decoder: sys::llama_model_has_decoder(m),
chat_template: self.chat_template(),
vision: self.projector.as_ref().is_some_and(|p| p.vision),
audio: self.projector.as_ref().is_some_and(|p| p.audio),
}
}
}
pub fn chat_template(&self) -> Option<String> {
let ptr = unsafe { sys::llama_model_chat_template(self.ptr.as_ptr(), std::ptr::null()) };
(!ptr.is_null()).then(|| unsafe { cstr_to_string(ptr) })
}
pub fn meta(&self, key: &str) -> Option<String> {
let key = CString::new(key).ok()?;
let mut buf = vec![0u8; 512];
loop {
let n = unsafe {
sys::llama_model_meta_val_str(
self.ptr.as_ptr(),
key.as_ptr(),
buf.as_mut_ptr() as *mut c_char,
buf.len(),
)
};
if n < 0 {
return None;
}
if (n as usize) < buf.len() {
buf.truncate(n as usize);
return Some(String::from_utf8_lossy(&buf).into_owned());
}
buf.resize(n as usize + 1, 0);
}
}
pub fn tokenize(&self, text: &str, add_special: bool, parse_special: bool) -> Result<Vec<i32>> {
let len = i32::try_from(text.len()).map_err(|_| Error::Tokenize)?;
let mut tokens = vec![0 as sys::llama_token; text.len() / 2 + 8];
for _ in 0..2 {
let n = unsafe {
sys::llama_tokenize(
self.vocab(),
text.as_ptr() as *const c_char,
len,
tokens.as_mut_ptr(),
tokens.len() as i32,
add_special,
parse_special,
)
};
if n >= 0 {
tokens.truncate(n as usize);
return Ok(tokens);
}
if n == i32::MIN {
break;
}
tokens.resize((-n) as usize, 0);
}
Err(Error::Tokenize)
}
pub(crate) fn token_bytes(&self, token: sys::llama_token, buf: &mut Vec<u8>, special: bool) {
buf.clear();
buf.resize(64, 0);
let mut n = unsafe {
sys::llama_token_to_piece(
self.vocab(),
token,
buf.as_mut_ptr() as *mut c_char,
buf.len() as i32,
0,
special,
)
};
if n < 0 {
buf.resize((-n) as usize, 0);
n = unsafe {
sys::llama_token_to_piece(
self.vocab(),
token,
buf.as_mut_ptr() as *mut c_char,
buf.len() as i32,
0,
special,
)
};
}
buf.truncate(n.max(0) as usize);
}
pub(crate) fn token_text(&self, token: sys::llama_token) -> String {
if token < 0 {
return String::new();
}
unsafe { crate::backend::cstr_to_string(sys::llama_vocab_get_text(self.vocab(), token)) }
}
pub(crate) fn is_eog(&self, token: sys::llama_token) -> bool {
unsafe { sys::llama_vocab_is_eog(self.vocab(), token) }
}
pub(crate) fn n_vocab(&self) -> i32 {
unsafe { sys::llama_vocab_n_tokens(self.vocab()) }
}
}
fn find_device(name: &str) -> Result<sys::ggml_backend_dev_t> {
let count = unsafe { sys::ggml_backend_dev_count() };
for i in 0..count {
let dev = unsafe { sys::ggml_backend_dev_get(i) };
if !dev.is_null() && unsafe { cstr_to_string(sys::ggml_backend_dev_name(dev)) } == name {
return Ok(dev);
}
}
Err(Error::InvalidArgument(format!("unknown device '{name}'")))
}