use std::collections::HashMap;
use std::io::{Cursor, Read};
use std::path::Path;
use crate::types::{DType, Weight, Weights};
#[derive(Debug)]
#[allow(dead_code)]
pub enum GgufValue {
U8(u8),
I8(i8),
U16(u16),
I16(i16),
U32(u32),
I32(i32),
F32(f32),
U64(u64),
I64(i64),
F64(f64),
Bool(bool),
Str(String),
Array(Vec<GgufValue>),
}
impl GgufValue {
pub fn as_u32(&self) -> Option<u32> {
match self {
GgufValue::U8(v) => Some(*v as u32),
GgufValue::I8(v) => Some(*v as u32),
GgufValue::U16(v) => Some(*v as u32),
GgufValue::I16(v) => Some(*v as u32),
GgufValue::U32(v) => Some(*v),
GgufValue::I32(v) => Some(*v as u32),
GgufValue::U64(v) => Some(*v as u32),
GgufValue::I64(v) => Some(*v as u32),
_ => None,
}
}
pub fn as_u64(&self) -> Option<u64> {
match self {
GgufValue::U8(v) => Some(*v as u64),
GgufValue::I8(v) => Some(*v as u64),
GgufValue::U16(v) => Some(*v as u64),
GgufValue::I16(v) => Some(*v as u64),
GgufValue::U32(v) => Some(*v as u64),
GgufValue::I32(v) => Some(*v as u64),
GgufValue::U64(v) => Some(*v),
GgufValue::I64(v) => Some(*v as u64),
_ => None,
}
}
pub fn as_f32(&self) -> Option<f32> {
match self {
GgufValue::F32(v) => Some(*v),
GgufValue::F64(v) => Some(*v as f32),
GgufValue::U32(v) => Some(*v as f32),
GgufValue::I32(v) => Some(*v as f32),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
GgufValue::Str(s) => Some(s.as_str()),
_ => None,
}
}
}
fn gguf_type_to_dtype(type_id: u32) -> DType {
match type_id {
0 => DType::F32, 1 => DType::F16, 2 => DType::Q4_0, 3 => DType::Q4_1, 8 => DType::Q8_0, 10 => DType::Q3_K, 11 => DType::Q2_K, 12 => DType::Q4_K, 13 => DType::Q5_K, 14 => DType::Q6_K, _ => {
log::error!("Unknown GGUF type {type_id} โ no dequant available, data will be wrong");
DType::F32
}
}
}
fn gguf_type_block_size(type_id: u32) -> usize {
match type_id {
0 => 1, 1 => 1, 2 => 32, 3 => 32, 8 => 32, 10 => 256, 11 => 256, 12 => 256, 13 => 256, 14 => 256, _ => 1,
}
}
fn gguf_type_bytes_per_block(type_id: u32) -> usize {
match type_id {
0 => 4, 1 => 2, 2 => 18, 3 => 20, 8 => 34, 10 => 110, 11 => 84, 12 => 144, 13 => 176, 14 => 210, _ => 4,
}
}
struct TensorInfo {
name: String,
dims: Vec<u64>,
type_id: u32,
offset: u64,
}
fn parse_gguf_header(
mmap: &[u8],
) -> Result<(HashMap<String, GgufValue>, Vec<TensorInfo>, usize), String> {
if mmap.len() < 24 {
return Err("File too small for GGUF".to_string());
}
let mut cursor = Cursor::new(mmap);
let mut magic = [0u8; 4];
cursor.read_exact(&mut magic).map_err(|e| e.to_string())?;
if &magic != b"GGUF" {
return Err(format!("Invalid GGUF magic: {:?}", magic));
}
let version = read_u32(&mut cursor)?;
log::info!("GGUF version: {version}");
let tensor_count = if version >= 3 {
read_u64(&mut cursor)?
} else {
read_u32(&mut cursor)? as u64
};
let metadata_kv_count = if version >= 3 {
read_u64(&mut cursor)?
} else {
read_u32(&mut cursor)? as u64
};
log::info!(
"GGUF: {tensor_count} tensors, {metadata_kv_count} metadata entries"
);
let mut metadata: HashMap<String, GgufValue> = HashMap::new();
for i in 0..metadata_kv_count {
let pos_before = cursor.position();
let key = read_gguf_string(&mut cursor)?;
let value = read_gguf_value(&mut cursor)?;
let pos_after = cursor.position();
log::debug!(
"GGUF metadata [{}/{}]: {} ({} bytes)",
i + 1,
metadata_kv_count,
key,
pos_after - pos_before,
);
metadata.insert(key, value);
}
if let Some(GgufValue::Str(arch)) = metadata.get("general.architecture") {
log::info!("Architecture: {arch}");
}
if let Some(GgufValue::Str(name)) = metadata.get("general.name") {
log::info!("Model name: {name}");
}
let mut tensor_infos = Vec::with_capacity(tensor_count as usize);
for _ in 0..tensor_count {
let name = read_gguf_string(&mut cursor)?;
let n_dims = read_u32(&mut cursor)?;
let mut dims = Vec::with_capacity(n_dims as usize);
for _ in 0..n_dims {
dims.push(read_u64(&mut cursor)?);
}
let type_id = read_u32(&mut cursor)?;
let offset = read_u64(&mut cursor)?;
tensor_infos.push(TensorInfo {
name,
dims,
type_id,
offset,
});
}
let header_end = cursor.position() as usize;
Ok((metadata, tensor_infos, header_end))
}
fn get_alignment(metadata: &HashMap<String, GgufValue>) -> usize {
metadata
.get("general.alignment")
.and_then(|v| v.as_u32())
.map(|v| v as usize)
.unwrap_or(32)
}
fn build_weights_from_gguf(
mmap: &[u8],
tensor_infos: &[TensorInfo],
data_start: usize,
) -> Weights {
let mut weights = Weights::new();
for info in tensor_infos {
let mut shape: Vec<usize> = info.dims.iter().map(|&d| d as usize).collect();
if shape.len() >= 2 {
shape.reverse();
}
let dtype = gguf_type_to_dtype(info.type_id);
let num_elements: usize = shape.iter().product();
let block_size = gguf_type_block_size(info.type_id);
let bytes_per_block = gguf_type_bytes_per_block(info.type_id);
let num_blocks = (num_elements + block_size - 1) / block_size;
let byte_size = num_blocks * bytes_per_block;
let abs_offset = data_start + info.offset as usize;
let abs_end = abs_offset + byte_size;
if abs_end > mmap.len() {
log::warn!(
"Tensor {} out of bounds: offset={} size={} end={} > file_size={}",
info.name, abs_offset, byte_size, abs_end, mmap.len(),
);
continue;
}
weights.insert(
info.name.clone(),
Weight {
data: mmap[abs_offset..abs_end].to_vec(),
shape,
dtype,
needs_transpose: false,
},
);
}
for suffix in [
"q_proj.weight",
"k_proj.weight",
"v_proj.weight",
"qkv_proj.weight",
"o_proj.weight",
"mlp.gate_proj.weight",
"mlp.up_proj.weight",
"mlp.gate_up_proj.weight",
"mlp.down_proj.weight",
] {
let name = format!("model.layers.0.self_attn.{suffix}");
let name2 = format!("model.layers.0.{suffix}");
let key = if weights.weights.contains_key(&name) {
&name
} else {
&name2
};
if let Some(w) = weights.weights.get(key) {
log::info!(
"GGUF {}: shape={:?} dtype={:?} bytes={}",
key, w.shape, w.dtype, w.data.len()
);
}
}
let fused_keys: Vec<String> = weights
.weights
.keys()
.filter(|k| k.contains("qkv_proj.weight") || k.contains("gate_up_proj.weight"))
.cloned()
.collect();
for key in fused_keys {
if let Some(w) = weights.weights.remove(&key) {
let n = w.shape[0];
let k = if w.shape.len() > 1 { w.shape[1] } else { 1 };
let prefix = key.rsplit_once('.').map(|(p, _)| p).unwrap_or(&key);
let sub_bytes = |sub_n: usize| -> usize {
let total = sub_n * k;
match w.dtype {
DType::Q4_0 => (total / 32) * 18,
DType::Q8_0 => (total / 32) * 34,
DType::F16 => total * 2,
_ => total * 4,
}
};
if key.contains("qkv_proj") {
let (q_n, kv_n) = if n % 3 == 0 { (n / 3, n / 3) } else { (n / 2, n / 4) };
let q_bytes = sub_bytes(q_n);
let kv_bytes = sub_bytes(kv_n);
let prefix = prefix.strip_suffix(".qkv_proj").unwrap_or(prefix);
if q_bytes + kv_bytes * 2 <= w.data.len() {
weights.insert(
format!("{prefix}.q_proj.weight"),
Weight {
data: w.data[..q_bytes].to_vec(),
shape: vec![q_n, k],
dtype: w.dtype,
needs_transpose: false,
},
);
weights.insert(
format!("{prefix}.k_proj.weight"),
Weight {
data: w.data[q_bytes..q_bytes + kv_bytes].to_vec(),
shape: vec![kv_n, k],
dtype: w.dtype,
needs_transpose: false,
},
);
weights.insert(
format!("{prefix}.v_proj.weight"),
Weight {
data: w.data[q_bytes + kv_bytes..q_bytes + kv_bytes * 2].to_vec(),
shape: vec![kv_n, k],
dtype: w.dtype,
needs_transpose: false,
},
);
log::info!("Split {key} โ q[{q_n},{k}] k[{kv_n},{k}] v[{kv_n},{k}]");
}
} else if key.contains("gate_up_proj") {
let half_n = n / 2;
let half_bytes = sub_bytes(half_n);
let prefix = prefix.strip_suffix(".gate_up_proj").unwrap_or(prefix);
if half_bytes * 2 <= w.data.len() {
weights.insert(
format!("{prefix}.gate_proj.weight"),
Weight {
data: w.data[..half_bytes].to_vec(),
shape: vec![half_n, k],
dtype: w.dtype,
needs_transpose: false,
},
);
weights.insert(
format!("{prefix}.up_proj.weight"),
Weight {
data: w.data[half_bytes..half_bytes * 2].to_vec(),
shape: vec![half_n, k],
dtype: w.dtype,
needs_transpose: false,
},
);
log::info!("Split {key} โ gate[{half_n},{k}] up[{half_n},{k}]");
}
}
}
}
weights
}
pub fn load_gguf(path: &Path) -> Result<Weights, String> {
load_gguf_with_metadata(path).map(|(w, _)| w)
}
pub fn load_gguf_with_metadata(
path: &Path,
) -> Result<(Weights, HashMap<String, GgufValue>), String> {
let file = std::fs::File::open(path)
.map_err(|e| format!("Cannot open {}: {e}", path.display()))?;
let mmap = unsafe {
memmap2::Mmap::map(&file)
.map_err(|e| format!("Cannot mmap {}: {e}", path.display()))?
};
let (metadata, tensor_infos, header_end) = parse_gguf_header(&mmap)?;
let alignment = get_alignment(&metadata);
let data_start = (header_end + alignment - 1) & !(alignment - 1);
log::info!(
"GGUF data_start: {} (header_end={}, alignment={})",
data_start, header_end, alignment,
);
let weights = build_weights_from_gguf(&mmap, &tensor_infos, data_start);
log::info!("GGUF loaded: {} tensors from {}", weights.len(), path.display());
Ok((weights, metadata))
}
fn read_u8(cursor: &mut Cursor<&[u8]>) -> Result<u8, String> {
let mut buf = [0u8; 1];
cursor.read_exact(&mut buf).map_err(|e| format!("read_u8: {e}"))?;
Ok(buf[0])
}
fn read_i8(cursor: &mut Cursor<&[u8]>) -> Result<i8, String> {
Ok(read_u8(cursor)? as i8)
}
fn read_u16(cursor: &mut Cursor<&[u8]>) -> Result<u16, String> {
let mut buf = [0u8; 2];
cursor.read_exact(&mut buf).map_err(|e| format!("read_u16: {e}"))?;
Ok(u16::from_le_bytes(buf))
}
fn read_i16(cursor: &mut Cursor<&[u8]>) -> Result<i16, String> {
let mut buf = [0u8; 2];
cursor.read_exact(&mut buf).map_err(|e| format!("read_i16: {e}"))?;
Ok(i16::from_le_bytes(buf))
}
fn read_u32(cursor: &mut Cursor<&[u8]>) -> Result<u32, String> {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf).map_err(|e| format!("read_u32: {e}"))?;
Ok(u32::from_le_bytes(buf))
}
fn read_i32(cursor: &mut Cursor<&[u8]>) -> Result<i32, String> {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf).map_err(|e| format!("read_i32: {e}"))?;
Ok(i32::from_le_bytes(buf))
}
fn read_u64(cursor: &mut Cursor<&[u8]>) -> Result<u64, String> {
let mut buf = [0u8; 8];
cursor.read_exact(&mut buf).map_err(|e| format!("read_u64: {e}"))?;
Ok(u64::from_le_bytes(buf))
}
fn read_i64(cursor: &mut Cursor<&[u8]>) -> Result<i64, String> {
let mut buf = [0u8; 8];
cursor.read_exact(&mut buf).map_err(|e| format!("read_i64: {e}"))?;
Ok(i64::from_le_bytes(buf))
}
fn read_f32_val(cursor: &mut Cursor<&[u8]>) -> Result<f32, String> {
let mut buf = [0u8; 4];
cursor.read_exact(&mut buf).map_err(|e| format!("read_f32: {e}"))?;
Ok(f32::from_le_bytes(buf))
}
fn read_f64_val(cursor: &mut Cursor<&[u8]>) -> Result<f64, String> {
let mut buf = [0u8; 8];
cursor.read_exact(&mut buf).map_err(|e| format!("read_f64: {e}"))?;
Ok(f64::from_le_bytes(buf))
}
fn read_gguf_string(cursor: &mut Cursor<&[u8]>) -> Result<String, String> {
let len = read_u64(cursor)? as usize;
if len > 1024 * 1024 * 64 {
return Err(format!("GGUF string too large: {len} bytes"));
}
let mut buf = vec![0u8; len];
cursor.read_exact(&mut buf).map_err(|e| format!("read_string({len}): {e}"))?;
String::from_utf8(buf).map_err(|e| format!("Invalid UTF-8 in GGUF string: {e}"))
}
fn read_gguf_value(cursor: &mut Cursor<&[u8]>) -> Result<GgufValue, String> {
let value_type = read_u32(cursor)?;
read_gguf_value_typed(cursor, value_type)
}
fn read_gguf_value_typed(cursor: &mut Cursor<&[u8]>, value_type: u32) -> Result<GgufValue, String> {
match value_type {
0 => Ok(GgufValue::U8(read_u8(cursor)?)),
1 => Ok(GgufValue::I8(read_i8(cursor)?)),
2 => Ok(GgufValue::U16(read_u16(cursor)?)),
3 => Ok(GgufValue::I16(read_i16(cursor)?)),
4 => Ok(GgufValue::U32(read_u32(cursor)?)),
5 => Ok(GgufValue::I32(read_i32(cursor)?)),
6 => Ok(GgufValue::F32(read_f32_val(cursor)?)),
7 => Ok(GgufValue::Bool(read_u8(cursor)? != 0)),
8 => Ok(GgufValue::Str(read_gguf_string(cursor)?)),
9 => {
let arr_type = read_u32(cursor)?;
let arr_len = read_u64(cursor)? as usize;
if arr_len > 100_000_000 {
return Err(format!("GGUF array too large: {arr_len} elements"));
}
let mut values = Vec::with_capacity(arr_len.min(1024 * 1024));
for _ in 0..arr_len {
let val = read_gguf_value_typed(cursor, arr_type)?;
values.push(val);
}
Ok(GgufValue::Array(values))
}
10 => Ok(GgufValue::U64(read_u64(cursor)?)),
11 => Ok(GgufValue::I64(read_i64(cursor)?)),
12 => Ok(GgufValue::F64(read_f64_val(cursor)?)),
_ => {
log::warn!("Unknown GGUF value type: {value_type} at position {}", cursor.position());
Err(format!("Unknown GGUF value type: {value_type}"))
}
}
}