use std::collections::HashMap;
use std::fmt::Debug;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use half::f16;
use crate::error::{Result, bail};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Type {
F32,
F16,
Q8_0,
}
impl Type {
fn from_ggml(id: u32) -> Option<Self> {
match id {
0 => Some(Self::F32),
1 => Some(Self::F16),
8 => Some(Self::Q8_0),
_ => None,
}
}
fn size(self, n: usize) -> usize {
match self {
Self::F32 => n * 4,
Self::F16 => n * 2,
Self::Q8_0 => n / 32 * 34,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Value<'a> {
Int(i64),
Float(f64),
Bool(bool),
Str(Option<&'a str>),
Array {
item: u32,
len: usize,
},
}
pub trait Meta<'a>: Sized {
fn from(v: Value<'a>) -> Option<Self>;
}
impl Meta<'_> for u32 {
fn from(v: Value) -> Option<Self> {
if let Value::Int(v) = v { v.try_into().ok() } else { None }
}
}
impl Meta<'_> for i32 {
fn from(v: Value) -> Option<Self> {
if let Value::Int(v) = v { v.try_into().ok() } else { None }
}
}
impl Meta<'_> for f32 {
fn from(v: Value) -> Option<Self> {
if let Value::Float(v) = v { Some(v as f32) } else { None }
}
}
impl Meta<'_> for bool {
fn from(v: Value) -> Option<Self> {
if let Value::Bool(v) = v { Some(v) } else { None }
}
}
impl<'a> Meta<'a> for &'a str {
fn from(v: Value<'a>) -> Option<Self> {
if let Value::Str(s) = v { s } else { None }
}
}
struct Info {
dims: Vec<usize>,
ty: Type,
offset: usize,
}
pub struct Gguf {
path: PathBuf,
head: Vec<u8>,
meta: HashMap<String, (u32, usize)>,
tensors: HashMap<String, Info>,
data: usize,
}
pub struct Tensor<'a> {
pub dims: &'a [usize],
pub ty: Type,
gguf: &'a Gguf,
start: usize,
size: usize,
}
impl Gguf {
pub fn open(path: &Path) -> Result<Self> {
let len = std::fs::metadata(path)?.len() as usize;
let mut n = len.min(1 << 20);
loop {
let mut head = vec![0; n];
File::open(path)?.read_exact(&mut head)?;
match Self::parse(head, len, path.into()) {
Err(_) if n < len => n = len.min(n * 4),
r => return r,
}
}
}
fn parse(head: Vec<u8>, len: usize, path: PathBuf) -> Result<Self> {
let mut r = Reader { bytes: &head, pos: 0 };
if r.take(4)? != b"GGUF" {
bail!("not a GGUF file");
}
let version = r.u32()?;
if version != 3 {
bail!("unsupported GGUF version {version}");
}
let n_tensors = r.u64()? as usize;
let n_meta = r.u64()? as usize;
let mut meta = HashMap::new();
for _ in 0..n_meta {
let key = r.string()?;
let ty = r.u32()?;
meta.insert(key, (ty, r.pos));
r.skip(ty)?;
}
let mut tensors = HashMap::new();
for _ in 0..n_tensors {
let name = r.string()?;
let n_dims = r.u32()? as usize;
let dims = (0..n_dims).map(|_| r.u64().map(|d| d as usize)).collect::<Result<Vec<_>>>()?;
let id = r.u32()?;
let Some(ty) = Type::from_ggml(id) else {
bail!("tensor {name}: unsupported ggml type {id}")
};
let offset = r.u64()? as usize;
tensors.insert(name, Info { dims, ty, offset });
}
let end = r.pos;
let mut g = Self { path, head, meta, tensors, data: 0 };
let align = match g.value("general.alignment") {
Ok((_, Value::Int(a))) => a as usize,
_ => 32,
};
g.data = end.next_multiple_of(align);
for (name, t) in &g.tensors {
if g.data + t.offset + t.ty.size(t.dims.iter().product()) > len {
bail!("tensor {name} is out of file bounds");
}
}
Ok(g)
}
fn value(&self, key: &str) -> Result<(Reader<'_>, Value<'_>)> {
let Some(&(ty, pos)) = self.meta.get(key) else { bail!("missing metadata {key}") };
let mut r = Reader { bytes: &self.head, pos };
let v = r.value(ty)?;
Ok((r, v))
}
pub fn get<'a, T: Meta<'a>>(&'a self, key: &str) -> Result<T> {
let (_, v) = self.value(key)?;
match T::from(v) {
Some(v) => Ok(v),
None => bail!("metadata {key}: expected {}, got {v:?}", std::any::type_name::<T>()),
}
}
pub fn check<'a, T: Meta<'a> + PartialEq + Debug>(
&'a self,
key: &str,
allowed: &[T],
) -> Result<()> {
let got = self.get::<T>(key)?;
if !allowed.contains(&got) {
bail!("{key} is {got:?}, only {allowed:?} supported");
}
Ok(())
}
pub fn array<'a, T: Meta<'a>>(&'a self, key: &str) -> Result<Vec<T>> {
let (mut r, Value::Array { item, len }) = self.value(key)? else {
bail!("metadata {key}: expected an array")
};
let items = (0..len).map(|_| r.value(item).map(T::from)).collect::<Result<Option<_>>>()?;
let Some(items) = items else {
bail!("metadata {key}: expected an array of {}", std::any::type_name::<T>())
};
Ok(items)
}
pub fn tensor(&self, name: &str, dims: &[usize]) -> Result<Tensor<'_>> {
let Some(t) = self.tensors.get(name) else { bail!("missing tensor {name}") };
if t.dims != dims {
bail!("tensor {name}: shape {:?}, expected {dims:?}", t.dims);
}
let (start, size) = (self.data + t.offset, t.ty.size(dims.iter().product()));
Ok(Tensor { dims: &t.dims, ty: t.ty, gguf: self, start, size })
}
}
impl Tensor<'_> {
pub fn len(&self) -> usize {
self.dims.iter().product()
}
pub fn read(&self, chunk: usize, mut f: impl FnMut(usize, &[u8])) -> Result<()> {
if let Some(bytes) = self.gguf.head.get(self.start..self.start + self.size) {
bytes.chunks(chunk).enumerate().for_each(|(i, c)| f(i * chunk, c));
return Ok(());
}
let mut file = File::open(&self.gguf.path)?;
file.seek(SeekFrom::Start(self.start as u64))?;
let mut buf = vec![0; chunk.min(self.size)];
for offset in (0..self.size).step_by(chunk) {
let piece = &mut buf[..chunk.min(self.size - offset)];
file.read_exact(piece)?;
f(offset, piece);
}
Ok(())
}
pub fn to_f32(&self) -> Result<Vec<f32>> {
let mut b = Vec::with_capacity(self.size);
self.read(self.size.max(1), |_, piece| b.extend_from_slice(piece))?;
Ok(match self.ty {
Type::F32 => b.as_chunks().0.iter().map(|&c| f32::from_le_bytes(c)).collect(),
Type::F16 => b.as_chunks().0.iter().map(|&c| f16::from_le_bytes(c).to_f32()).collect(),
Type::Q8_0 => (b.as_chunks::<34>().0.iter())
.flat_map(|block| {
let d = f16::from_le_bytes([block[0], block[1]]).to_f32();
block[2..].iter().map(move |&q| q as i8 as f32 * d)
})
.collect(),
})
}
}
#[cfg(test)]
impl Gguf {
pub(crate) fn with_tensors(tensors: &[(&str, &[usize], Type, &[u8])]) -> Self {
let mut head = Vec::new();
let mut infos = HashMap::new();
for &(name, dims, ty, data) in tensors {
infos.insert(name.to_string(), Info { dims: dims.to_vec(), ty, offset: head.len() });
head.extend_from_slice(data);
}
Self { path: PathBuf::new(), head, meta: HashMap::new(), tensors: infos, data: 0 }
}
}
struct Reader<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
let Some(s) = self.bytes.get(self.pos..self.pos + n) else { bail!("truncated GGUF header") };
self.pos += n;
Ok(s)
}
fn array<const N: usize>(&mut self) -> Result<[u8; N]> {
Ok(self.take(N)?.try_into().unwrap())
}
fn u32(&mut self) -> Result<u32> {
Ok(u32::from_le_bytes(self.array()?))
}
fn u64(&mut self) -> Result<u64> {
Ok(u64::from_le_bytes(self.array()?))
}
fn bytes(&mut self) -> Result<&'a [u8]> {
let n = self.u64()? as usize;
self.take(n)
}
fn string(&mut self) -> Result<String> {
Ok(String::from_utf8_lossy(self.bytes()?).into_owned())
}
fn skip(&mut self, ty: u32) -> Result<()> {
let size = |ty| match ty {
0 | 1 | 7 => Some(1),
2 | 3 => Some(2),
4..=6 => Some(4),
10..=12 => Some(8),
_ => None,
};
match ty {
8 => _ = self.bytes()?,
9 => {
let item = self.u32()?;
let n = self.u64()? as usize;
match size(item) {
Some(size) => _ = self.take(n * size)?,
None => (0..n).try_for_each(|_| self.skip(item))?,
}
}
_ => match size(ty) {
Some(size) => _ = self.take(size)?,
None => bail!("unknown GGUF value type {ty}"),
},
}
Ok(())
}
fn value(&mut self, ty: u32) -> Result<Value<'a>> {
Ok(match ty {
0 => Value::Int(self.array::<1>()?[0] as i64),
1 => Value::Int(self.array::<1>()?[0] as i8 as i64),
2 => Value::Int(u16::from_le_bytes(self.array()?) as i64),
3 => Value::Int(i16::from_le_bytes(self.array()?) as i64),
4 => Value::Int(self.u32()? as i64),
5 => Value::Int(i32::from_le_bytes(self.array()?) as i64),
6 => Value::Float(f32::from_le_bytes(self.array()?) as f64),
7 => Value::Bool(self.array::<1>()?[0] != 0),
8 => Value::Str(std::str::from_utf8(self.bytes()?).ok()),
9 => Value::Array { item: self.u32()?, len: self.u64()? as usize },
10 => Value::Int(self.u64()? as i64),
11 => Value::Int(i64::from_le_bytes(self.array()?)),
12 => Value::Float(f64::from_le_bytes(self.array()?)),
_ => bail!("unknown GGUF value type {ty}"),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn string(out: &mut Vec<u8>, s: &str) {
out.extend((s.len() as u64).to_le_bytes());
out.extend(s.as_bytes());
}
fn sample() -> Vec<u8> {
let mut f = b"GGUF".to_vec();
f.extend(3u32.to_le_bytes());
f.extend(2u64.to_le_bytes());
f.extend(6u64.to_le_bytes());
string(&mut f, "a.n");
f.extend(4u32.to_le_bytes());
f.extend(7u32.to_le_bytes());
string(&mut f, "a.tokens");
f.extend(9u32.to_le_bytes());
f.extend(8u32.to_le_bytes());
f.extend(2u64.to_le_bytes());
string(&mut f, "▁x");
string(&mut f, "y");
string(&mut f, "a.flag");
f.extend(7u32.to_le_bytes());
f.push(1);
string(&mut f, "a.left");
f.extend(5u32.to_le_bytes());
f.extend((-1i32).to_le_bytes());
string(&mut f, "a.durations");
f.extend(9u32.to_le_bytes());
f.extend(5u32.to_le_bytes());
f.extend(3u64.to_le_bytes());
[0i32, 2, -4].iter().for_each(|v| f.extend(v.to_le_bytes()));
string(&mut f, "a.alpha");
f.extend(6u32.to_le_bytes());
f.extend(0.97f32.to_le_bytes());
string(&mut f, "w");
f.extend(2u32.to_le_bytes());
f.extend(2u64.to_le_bytes());
f.extend(1u64.to_le_bytes());
f.extend(0u32.to_le_bytes());
f.extend(0u64.to_le_bytes());
string(&mut f, "q");
f.extend(1u32.to_le_bytes());
f.extend(32u64.to_le_bytes());
f.extend(8u32.to_le_bytes());
f.extend(32u64.to_le_bytes());
f.resize(f.len().next_multiple_of(32), 0);
f.extend(1.5f32.to_le_bytes());
f.extend((-2.0f32).to_le_bytes());
f.resize(f.len().next_multiple_of(32), 0);
f.extend(f16::from_f32(0.5).to_le_bytes());
f.extend((0..32).map(|i| (i as i8 - 16) as u8));
f
}
fn parse(f: Vec<u8>) -> Result<Gguf> {
let len = f.len();
Gguf::parse(f, len, PathBuf::new())
}
#[test]
fn reads_metadata_and_tensors() {
let g = parse(sample()).unwrap();
assert_eq!(g.get::<u32>("a.n").unwrap(), 7);
assert_eq!(g.get::<i32>("a.n").unwrap(), 7);
assert!(g.get::<u32>("missing").is_err());
assert!(g.get::<&str>("a.n").is_err());
assert_eq!(g.array::<&str>("a.tokens").unwrap(), ["▁x", "y"]);
assert!(g.get::<bool>("a.flag").unwrap());
assert_eq!(g.get::<i32>("a.left").unwrap(), -1);
assert!(g.get::<u32>("a.left").is_err());
assert_eq!(g.array::<i32>("a.durations").unwrap(), [0, 2, -4]);
assert!(g.array::<u32>("a.durations").is_err());
assert_eq!(g.get::<f32>("a.alpha").unwrap(), 0.97);
assert_eq!(g.tensor("w", &[2, 1]).unwrap().to_f32().unwrap(), [1.5, -2.0]);
assert!(g.tensor("w", &[1, 2]).is_err());
let q = g.tensor("q", &[32]).unwrap();
assert_eq!(q.ty, Type::Q8_0);
let expected: Vec<f32> = (0..32).map(|i| (i - 16) as f32 * 0.5).collect();
assert_eq!(q.to_f32().unwrap(), expected);
let mut pieces = Vec::new();
q.read(16, |offset, piece| pieces.push((offset, piece.len()))).unwrap();
assert_eq!(pieces, [(0, 16), (16, 16), (32, 2)]);
}
#[test]
fn rejects_truncated_files() {
let mut f = sample();
f.truncate(f.len() - 1);
assert!(parse(f).is_err());
}
}