use std::collections::HashMap;
use std::fs;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use candle_core::DType;
use serde::Deserialize;
use super::npy;
use crate::error::CanddingError;
use crate::error::Result;
const MAX_HEADER_BYTES: u64 = 100_000_000;
#[derive(Deserialize)]
struct TensorEntry {
shape: Vec<u64>,
}
pub(crate) fn resident_bytes(path: &Path, dtype: DType) -> Result<u64> {
match path.extension().and_then(|ext| ext.to_str()) {
Some("safetensors") => {
Ok(safetensors_elements(path)?.saturating_mul(dtype.size_in_bytes() as u64))
}
Some("npy") => Ok(scale(
fs::metadata(path)?.len(),
npy::stored_dtype(path)?,
dtype,
)),
_ => Ok(scale(fs::metadata(path)?.len(), pickle_dtype(path)?, dtype)),
}
}
pub(crate) fn scale(bytes: u64, from: DType, to: DType) -> u64 {
let scaled = u128::from(bytes) * to.size_in_bytes() as u128 / from.size_in_bytes() as u128;
u64::try_from(scaled).unwrap_or(u64::MAX)
}
fn safetensors_elements(path: &Path) -> Result<u64> {
let mut file = File::open(path)?;
let mut length = [0u8; 8];
file
.read_exact(&mut length)
.map_err(|e| unreadable(path, &e.to_string()))?;
let length = u64::from_le_bytes(length);
if length > MAX_HEADER_BYTES {
return Err(unreadable(
path,
&format!("its header claims {length} bytes"),
));
}
let mut header = vec![0u8; length as usize];
file
.read_exact(&mut header)
.map_err(|e| unreadable(path, &e.to_string()))?;
let mut entries: HashMap<String, serde_json::Value> =
serde_json::from_slice(&header).map_err(|e| unreadable(path, &e.to_string()))?;
entries.remove("__metadata__");
entries.into_values().try_fold(0u64, |total, entry| {
let entry: TensorEntry =
serde_json::from_value(entry).map_err(|e| unreadable(path, &e.to_string()))?;
let elements = entry
.shape
.iter()
.fold(1u64, |n, &dim| n.saturating_mul(dim));
Ok(total.saturating_add(elements))
})
}
fn pickle_dtype(path: &Path) -> Result<DType> {
let infos = candle_core::pickle::read_pth_tensor_info(path, false, None)?;
let largest = infos
.iter()
.max_by_key(|info| info.layout.shape().elem_count());
Ok(largest.map_or(DType::F32, |info| info.dtype))
}
fn unreadable(path: &Path, reason: &str) -> CanddingError {
candle_core::Error::Msg(format!(
"`{}` has no readable safetensors header: {reason}",
path.display()
))
.into()
}
#[cfg(test)]
mod tests;