use crate::error::{Result, RustyGradientsError};
use crate::tensor::Tensor;
use super::ModelMetadata; use safetensors::tensor::{Dtype, SafeTensors};
use safetensors::SafeTensorError;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
pub fn save_model(
path: &Path,
weights: &[Tensor],
weight_names: &[String],
metadata: &ModelMetadata,
) -> Result<()> {
if weights.len() != weight_names.len() {
return Err(RustyGradientsError::SerializationError(
"weights and weight_names must have same length".to_string(),
));
}
let mut tensor_map: HashMap<String, (Vec<u8>, Dtype, Vec<usize>)> = HashMap::new();
for (name, tensor) in weight_names.iter().zip(weights.iter()) {
let data_ref = tensor.data.borrow();
let shape: Vec<usize> = data_ref.shape().to_vec();
let data_f32: Vec<f32> = data_ref.iter().cloned().collect();
let data_bytes: Vec<u8> = data_f32
.iter()
.flat_map(|f| f.to_le_bytes())
.collect();
tensor_map.insert(name.clone(), (data_bytes, Dtype::F32, shape));
}
let tensor_views: HashMap<String, safetensors::tensor::TensorView<'_>> = tensor_map
.iter()
.map(|(k, (data, dtype, shape))| {
let view = safetensors::tensor::TensorView::new(*dtype, shape.clone(), data.as_slice())
.expect("TensorView creation failed");
(k.clone(), view)
})
.collect();
let serialized = safetensors::tensor::serialize(&tensor_views, &None)?;
let mut file = File::create(path)
.map_err(|e| RustyGradientsError::IoError(e.to_string()))?;
file.write_all(&serialized)
.map_err(|e| RustyGradientsError::IoError(e.to_string()))?;
let metadata_path = path.with_extension("safetensors.json");
let metadata_json = serde_json::to_string_pretty(metadata)
.map_err(|e| RustyGradientsError::SerializationError(e.to_string()))?;
std::fs::write(metadata_path, metadata_json)
.map_err(|e| RustyGradientsError::IoError(e.to_string()))?;
Ok(())
}
pub fn load_model(
path: &Path,
) -> Result<(Vec<Vec<f32>>, Vec<Vec<usize>>, Vec<String>, ModelMetadata)> {
let mut file = File::open(path)
.map_err(|e| RustyGradientsError::IoError(e.to_string()))?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)
.map_err(|e| RustyGradientsError::IoError(e.to_string()))?;
let tensors = SafeTensors::deserialize(&buffer)?;
let mut weight_data = Vec::new();
let mut weight_shapes = Vec::new();
let mut weight_names = Vec::new();
for name in tensors.names() {
let tensor_view = tensors.tensor(name)?;
if tensor_view.dtype() != Dtype::F32 {
return Err(RustyGradientsError::SerializationError(format!(
"Expected F32 dtype, got {:?}",
tensor_view.dtype()
)));
}
let data_bytes = tensor_view.data();
let data: Vec<f32> = data_bytes
.chunks_exact(4)
.map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
weight_data.push(data);
weight_shapes.push(tensor_view.shape().to_vec());
weight_names.push(name.to_string());
}
let metadata_path = path.with_extension("safetensors.json");
let metadata_json = std::fs::read_to_string(metadata_path)
.map_err(|e| RustyGradientsError::IoError(e.to_string()))?;
let metadata: ModelMetadata = serde_json::from_str(&metadata_json)
.map_err(|e| RustyGradientsError::SerializationError(e.to_string()))?;
Ok((weight_data, weight_shapes, weight_names, metadata))
}
#[cfg(feature = "serialization")]
pub fn load_model_mmap(path: &Path) -> Result<(SafeTensors<'static>, ModelMetadata)> {
use memmap2::Mmap;
let file = File::open(path)
.map_err(|e| RustyGradientsError::IoError(e.to_string()))?;
let mmap = unsafe {
Mmap::map(&file)
.map_err(|e| RustyGradientsError::IoError(e.to_string()))?
};
let buffer: &'static [u8] = Box::leak(mmap.to_vec().into_boxed_slice());
let tensors = SafeTensors::deserialize(buffer)?;
let metadata_path = path.with_extension("safetensors.json");
let metadata_json = std::fs::read_to_string(metadata_path)
.map_err(|e| RustyGradientsError::IoError(e.to_string()))?;
let metadata: ModelMetadata = serde_json::from_str(&metadata_json)
.map_err(|e| RustyGradientsError::SerializationError(e.to_string()))?;
Ok((tensors, metadata))
}
impl From<SafeTensorError> for RustyGradientsError {
fn from(err: SafeTensorError) -> Self {
RustyGradientsError::SerializationError(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tensor::Tensor;
use ndarray::ArrayD;
use std::path::PathBuf;
#[test]
fn test_save_load_roundtrip() {
let t1 = Tensor::new(
ArrayD::from_shape_vec(vec![2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
.unwrap()
.into_dyn(),
false,
);
let t2 = Tensor::new(
ArrayD::from_shape_vec(vec![3], vec![0.1, 0.2, 0.3])
.unwrap()
.into_dyn(),
false,
);
let weights = vec![t1, t2];
let names = vec!["layer1.weight".to_string(), "layer1.bias".to_string()];
let metadata = ModelMetadata {
model_type: "TestModel".to_string(),
vocab_size: 100,
embedding_dim: 64,
num_layers: 2,
num_heads: 4,
block_size: 128,
dropout: 0.1,
};
let temp_path = PathBuf::from("test_model.safetensors");
save_model(&temp_path, &weights, &names, &metadata).unwrap();
let (loaded_data, loaded_shapes, loaded_names, loaded_metadata) =
load_model(&temp_path).unwrap();
assert_eq!(loaded_names.len(), 2);
let weight_idx = loaded_names.iter().position(|n| n == "layer1.weight").unwrap();
let bias_idx = loaded_names.iter().position(|n| n == "layer1.bias").unwrap();
assert_eq!(loaded_shapes[weight_idx], vec![2, 3]);
assert_eq!(loaded_shapes[bias_idx], vec![3]);
assert_eq!(loaded_metadata.model_type, "TestModel");
std::fs::remove_file(&temp_path).ok();
std::fs::remove_file(temp_path.with_extension("safetensors.json")).ok();
}
}