use std::collections::BTreeMap;
use std::path::Path;
use indexmap::IndexMap;
use kopitiam_core::{DType, Error, Result, Shape};
use serde::Deserialize;
use crate::byte_source::ByteSource;
use crate::metadata::{GgufMetadata, GgufValue, ModelMetadata};
use crate::model::{LoadedModel, ModelLoader, TensorEntry};
const FORMAT: &str = "safetensors";
const HEADER_LEN_BYTES: usize = 8;
const METADATA_KEY: &str = "__metadata__";
fn malformed(reason: impl Into<String>) -> Error {
Error::MalformedModel { format: FORMAT, reason: reason.into() }
}
fn unsupported(feature: impl Into<String>) -> Error {
Error::UnsupportedModelFeature { format: FORMAT, feature: feature.into() }
}
#[derive(Debug, Deserialize)]
struct RawTensorInfo {
dtype: String,
shape: Vec<u64>,
data_offsets: (u64, u64),
}
fn dtype_from_str(s: &str) -> Result<DType> {
match s {
"F32" => Ok(DType::F32),
"F16" => Ok(DType::F16),
"BF16" => Ok(DType::BF16),
"I8" => Ok(DType::I8),
"I32" => Ok(DType::I32),
other => Err(unsupported(format!("safetensors dtype {other:?}"))),
}
}
fn parse(source: ByteSource) -> Result<LoadedModel> {
let bytes = source.as_slice();
let header_len_bytes = bytes.get(..HEADER_LEN_BYTES).ok_or_else(|| {
malformed(format!(
"file is {} bytes, shorter than the {HEADER_LEN_BYTES}-byte header length prefix",
bytes.len()
))
})?;
let header_len = u64::from_le_bytes(
header_len_bytes.try_into().expect("checked slice is exactly 8 bytes"),
);
let header_len = usize::try_from(header_len)
.map_err(|_| malformed(format!("header length {header_len} does not fit in memory")))?;
let data_start = HEADER_LEN_BYTES
.checked_add(header_len)
.ok_or_else(|| malformed("header end offset overflows"))?;
let header_bytes = bytes.get(HEADER_LEN_BYTES..data_start).ok_or_else(|| {
malformed(format!(
"declared header length {header_len} extends past end of file ({} bytes)",
bytes.len()
))
})?;
let header: serde_json::Map<String, serde_json::Value> = serde_json::from_slice(header_bytes)
.map_err(|e| malformed(format!("header is not valid JSON: {e}")))?;
let mut raw_metadata = GgufMetadata::new();
let mut tensors = IndexMap::new();
let file_len = bytes.len();
for (name, value) in header {
if name == METADATA_KEY {
let entries: BTreeMap<String, String> = serde_json::from_value(value).map_err(|e| {
malformed(format!("{METADATA_KEY} must map strings to strings: {e}"))
})?;
for (k, v) in entries {
raw_metadata.0.insert(k, GgufValue::String(v));
}
continue;
}
let info: RawTensorInfo = serde_json::from_value(value).map_err(|e| {
malformed(format!("tensor {name:?} header entry is malformed: {e}"))
})?;
let dtype = dtype_from_str(&info.dtype)?;
let mut dims = Vec::with_capacity(info.shape.len());
for d in info.shape {
let d = usize::try_from(d).map_err(|_| {
malformed(format!("tensor {name:?} has a dimension ({d}) too large to represent"))
})?;
dims.push(d);
}
let shape = Shape::new(dims);
let elem_count = shape.elem_count();
let expected_len = dtype.storage_bytes(elem_count).ok_or(Error::PartialQuantizedBlock {
dtype,
count: elem_count,
block_size: dtype.block_size(),
})?;
let (start, end) = info.data_offsets;
if start > end {
return Err(malformed(format!(
"tensor {name:?} has data_offsets start ({start}) after end ({end})"
)));
}
let declared_len = end - start;
if declared_len != expected_len as u64 {
return Err(malformed(format!(
"tensor {name:?} declares {declared_len} data bytes but its dtype ({dtype}) and shape ({shape}) need {expected_len}"
)));
}
let abs_offset = (data_start as u64).checked_add(start).ok_or_else(|| {
malformed(format!("tensor {name:?} data offset overflows a u64"))
})?;
let abs_end = (data_start as u64).checked_add(end).ok_or_else(|| {
malformed(format!("tensor {name:?} data end offset overflows a u64"))
})?;
if abs_end > file_len as u64 {
return Err(malformed(format!(
"tensor {name:?} data range [{abs_offset}, {abs_end}) extends past end of file ({file_len} bytes)"
)));
}
let abs_offset = usize::try_from(abs_offset)
.map_err(|_| malformed(format!("tensor {name:?} offset does not fit in memory")))?;
let entry = TensorEntry {
name: name.clone(),
dtype,
shape,
offset: abs_offset,
len: expected_len,
};
if tensors.insert(name.clone(), entry).is_some() {
return Err(malformed(format!("duplicate tensor name {name:?}")));
}
}
let metadata = ModelMetadata {
architecture: None,
name: None,
n_layers: None,
n_heads: None,
n_kv_heads: None,
embedding_length: None,
feed_forward_length: None,
context_length: None,
vocab_size: None,
rope_theta: None,
rope_dimension_count: None,
norm_epsilon: None,
quantization_version: None,
file_type: None,
raw: raw_metadata,
};
Ok(LoadedModel { metadata, tensors, source, format: FORMAT })
}
pub struct SafeTensorsLoader;
impl ModelLoader for SafeTensorsLoader {
fn format_name(&self) -> &'static str {
FORMAT
}
fn probe(&self, bytes: &[u8]) -> bool {
let Some(len_bytes) = bytes.get(..HEADER_LEN_BYTES) else {
return false;
};
let header_len =
u64::from_le_bytes(len_bytes.try_into().expect("checked slice is exactly 8 bytes"));
if header_len == 0 {
return false;
}
match bytes.get(HEADER_LEN_BYTES) {
Some(b'{') => true,
Some(_) => false,
None => true,
}
}
fn load(&self, path: &Path) -> Result<LoadedModel> {
let source = ByteSource::open(path)?;
parse(source)
}
}