Skip to main content

ipfrs_interface/
safetensors.rs

1//! Safetensors integration for tensor serialization
2//!
3//! Provides utilities for working with safetensors format:
4//! - Parsing and validating safetensors files
5//! - Extracting tensor metadata
6//! - Reading tensor data
7//! - Creating safetensors from raw tensors
8
9use bytes::Bytes;
10use ipfrs_core::error::{Error, Result};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14/// Safetensors file format handler
15#[derive(Debug)]
16pub struct SafetensorsFile {
17    /// Parsed header with tensor metadata
18    header: SafetensorsHeader,
19    /// Raw data bytes (header + tensors)
20    data: Bytes,
21    /// Header size in bytes
22    header_size: usize,
23}
24
25/// Safetensors header structure
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SafetensorsHeader {
28    /// Tensor metadata indexed by name
29    #[serde(flatten)]
30    pub tensors: HashMap<String, TensorInfo>,
31}
32
33/// Information about a single tensor
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct TensorInfo {
36    /// Data type (e.g., "F32", "F64", "I32")
37    pub dtype: String,
38    /// Tensor shape (dimensions)
39    pub shape: Vec<usize>,
40    /// Start offset in the data section
41    pub data_offsets: [usize; 2], // [start, end]
42}
43
44impl SafetensorsFile {
45    /// Parse a safetensors file from bytes
46    pub fn from_bytes(data: Bytes) -> Result<Self> {
47        if data.len() < 8 {
48            return Err(Error::InvalidInput(
49                "Data too short for safetensors format".to_string(),
50            ));
51        }
52
53        // First 8 bytes = header length (little-endian u64)
54        let header_len = u64::from_le_bytes(
55            data[0..8]
56                .try_into()
57                .expect("data[0..8] is exactly 8 bytes after bounds check"),
58        ) as usize;
59
60        if data.len() < 8 + header_len {
61            return Err(Error::InvalidInput(
62                "Incomplete safetensors header".to_string(),
63            ));
64        }
65
66        // Parse JSON header
67        let header_bytes = &data[8..8 + header_len];
68        let header: SafetensorsHeader = serde_json::from_slice(header_bytes).map_err(|e| {
69            Error::InvalidInput(format!("Failed to parse safetensors header: {}", e))
70        })?;
71
72        // Validate header
73        Self::validate_header(&header, data.len() - 8 - header_len)?;
74
75        Ok(SafetensorsFile {
76            header,
77            data,
78            header_size: 8 + header_len,
79        })
80    }
81
82    /// Validate header offsets and data integrity
83    fn validate_header(header: &SafetensorsHeader, data_section_size: usize) -> Result<()> {
84        for (name, info) in &header.tensors {
85            let [start, end] = info.data_offsets;
86
87            if start >= end {
88                return Err(Error::InvalidInput(format!(
89                    "Invalid offsets for tensor '{}': start={}, end={}",
90                    name, start, end
91                )));
92            }
93
94            if end > data_section_size {
95                return Err(Error::InvalidInput(format!(
96                    "Tensor '{}' offset {} exceeds data section size {}",
97                    name, end, data_section_size
98                )));
99            }
100
101            // Validate size matches shape and dtype
102            let expected_size = Self::calculate_tensor_size(&info.shape, &info.dtype);
103            let actual_size = end - start;
104
105            if actual_size != expected_size {
106                return Err(Error::InvalidInput(format!(
107                    "Tensor '{}' size mismatch: expected {}, got {}",
108                    name, expected_size, actual_size
109                )));
110            }
111        }
112
113        Ok(())
114    }
115
116    /// Calculate expected tensor size in bytes
117    fn calculate_tensor_size(shape: &[usize], dtype: &str) -> usize {
118        let num_elements: usize = shape.iter().product();
119        let element_size = Self::dtype_size(dtype);
120        num_elements * element_size
121    }
122
123    /// Get size of a data type in bytes
124    fn dtype_size(dtype: &str) -> usize {
125        match dtype {
126            "F16" | "BF16" => 2,
127            "F32" | "I32" | "U32" => 4,
128            "F64" | "I64" | "U64" => 8,
129            "I8" | "U8" => 1,
130            "I16" | "U16" => 2,
131            "BOOL" => 1,
132            _ => 4, // Default to 4 bytes
133        }
134    }
135
136    /// Get tensor data by name
137    pub fn get_tensor(&self, name: &str) -> Result<TensorData> {
138        let info = self.header.tensors.get(name).ok_or_else(|| {
139            Error::NotFound(format!("Tensor '{}' not found in safetensors file", name))
140        })?;
141
142        let [start, end] = info.data_offsets;
143        let data_start = self.header_size + start;
144        let data_end = self.header_size + end;
145
146        if data_end > self.data.len() {
147            return Err(Error::InvalidInput(format!(
148                "Tensor data range {}..{} exceeds file size {}",
149                data_start,
150                data_end,
151                self.data.len()
152            )));
153        }
154
155        Ok(TensorData {
156            dtype: info.dtype.clone(),
157            shape: info.shape.clone(),
158            data: self.data.slice(data_start..data_end),
159        })
160    }
161
162    /// Get all tensor names
163    pub fn tensor_names(&self) -> Vec<String> {
164        self.header
165            .tensors
166            .keys()
167            .filter(|k| k.as_str() != "__metadata__")
168            .cloned()
169            .collect()
170    }
171
172    /// Get tensor metadata by name
173    pub fn get_tensor_info(&self, name: &str) -> Option<&TensorInfo> {
174        self.header.tensors.get(name)
175    }
176
177    /// Get the full header
178    pub fn header(&self) -> &SafetensorsHeader {
179        &self.header
180    }
181
182    /// Get raw file data
183    pub fn raw_data(&self) -> &Bytes {
184        &self.data
185    }
186}
187
188/// Tensor data extracted from safetensors
189#[derive(Debug, Clone)]
190pub struct TensorData {
191    /// Data type
192    pub dtype: String,
193    /// Shape (dimensions)
194    pub shape: Vec<usize>,
195    /// Raw tensor data
196    pub data: Bytes,
197}
198
199impl TensorData {
200    /// Get the number of elements in the tensor
201    pub fn num_elements(&self) -> usize {
202        self.shape.iter().product()
203    }
204
205    /// Get the size in bytes
206    pub fn size_bytes(&self) -> usize {
207        self.data.len()
208    }
209
210    /// Get element size in bytes
211    pub fn element_size(&self) -> usize {
212        if self.num_elements() == 0 {
213            return 0;
214        }
215        self.size_bytes() / self.num_elements()
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_dtype_size() {
225        assert_eq!(SafetensorsFile::dtype_size("F32"), 4);
226        assert_eq!(SafetensorsFile::dtype_size("F64"), 8);
227        assert_eq!(SafetensorsFile::dtype_size("F16"), 2);
228        assert_eq!(SafetensorsFile::dtype_size("I32"), 4);
229        assert_eq!(SafetensorsFile::dtype_size("U8"), 1);
230        assert_eq!(SafetensorsFile::dtype_size("BOOL"), 1);
231    }
232
233    #[test]
234    fn test_calculate_tensor_size() {
235        assert_eq!(
236            SafetensorsFile::calculate_tensor_size(&[10, 20], "F32"),
237            10 * 20 * 4
238        );
239        assert_eq!(
240            SafetensorsFile::calculate_tensor_size(&[5, 5, 5], "F64"),
241            5 * 5 * 5 * 8
242        );
243    }
244
245    #[test]
246    fn test_tensor_data_num_elements() {
247        let data = TensorData {
248            dtype: "F32".to_string(),
249            shape: vec![2, 3],
250            data: Bytes::from(vec![0u8; 24]), // 2*3*4 = 24 bytes
251        };
252        assert_eq!(data.num_elements(), 6);
253        assert_eq!(data.size_bytes(), 24);
254        assert_eq!(data.element_size(), 4);
255    }
256}