1use std::collections::BTreeMap;
10use std::path::Path;
11
12use nuvec_core::{Array, DType, Error, MappedFile, Result};
13
14use crate::json::{self, Json};
15
16fn invalid(msg: impl Into<String>) -> Error {
17 Error::InvalidArgument(format!("safetensors: {}", msg.into()))
18}
19
20#[derive(Debug, Clone, Default)]
22pub struct SafeTensors {
23 pub tensors: BTreeMap<String, Array>,
25 pub metadata: BTreeMap<String, String>,
27}
28
29fn dtype_of(name: &str) -> Option<DType> {
31 Some(match name {
32 "BOOL" => DType::Bool,
33 "U8" => DType::U8,
34 "U16" => DType::U16,
35 "U32" => DType::U32,
36 "U64" => DType::U64,
37 "I8" => DType::I8,
38 "I16" => DType::I16,
39 "I32" => DType::I32,
40 "I64" => DType::I64,
41 "F16" => DType::F16,
42 "BF16" => DType::BF16,
43 "F32" => DType::F32,
44 "F64" => DType::F64,
45 "C64" => DType::Complex64,
46 _ => return None,
47 })
48}
49
50fn name_of(dtype: DType) -> &'static str {
51 match dtype {
52 DType::Bool => "BOOL",
53 DType::U8 => "U8",
54 DType::U16 => "U16",
55 DType::U32 => "U32",
56 DType::U64 => "U64",
57 DType::I8 => "I8",
58 DType::I16 => "I16",
59 DType::I32 => "I32",
60 DType::I64 => "I64",
61 DType::F16 => "F16",
62 DType::BF16 => "BF16",
63 DType::F32 => "F32",
64 DType::F64 => "F64",
65 _ => "C64",
66 }
67}
68
69fn usize_of(v: &Json, what: &str) -> Result<usize> {
70 match v {
71 Json::Int(n) if *n >= 0 => Ok(*n as usize),
72 _ => Err(invalid(format!("{what} must be a non-negative integer"))),
73 }
74}
75
76const MAX_HEADER: usize = 100_000_000;
78
79pub fn load_safetensors(path: impl AsRef<Path>) -> Result<SafeTensors> {
86 let file = MappedFile::open(path)?;
87 let bytes = file.bytes();
88 let size = bytes
89 .get(..8)
90 .map(|b| u64::from_le_bytes(b.try_into().expect("8 bytes")))
91 .ok_or_else(|| invalid("file shorter than its header size"))?;
92 let start = usize::try_from(size)
93 .ok()
94 .filter(|&n| n <= MAX_HEADER)
95 .and_then(|n| n.checked_add(8))
96 .filter(|&end| end <= bytes.len())
97 .ok_or_else(|| invalid("header larger than the file or 100 MB"))?;
98 let Json::Object(entries) = json::parse(&bytes[8..start])? else {
99 return Err(invalid("the header must be a JSON object"));
100 };
101 let mut out = SafeTensors::default();
102 let mut ranges = Vec::new();
103 for (name, entry) in entries {
104 if name == "__metadata__" {
105 match entry {
106 Json::Null => {}
107 Json::Object(fields) => {
108 for (k, v) in fields {
109 let Json::Str(s) = v else {
110 return Err(invalid("metadata values must be strings"));
111 };
112 out.metadata.insert(k, s);
113 }
114 }
115 _ => return Err(invalid("`__metadata__` must be an object")),
116 }
117 continue;
118 }
119 let Json::Object(fields) = entry else {
120 return Err(invalid(format!("entry `{name}` must be an object")));
121 };
122 let field = |key: &str| fields.iter().find(|(k, _)| k == key).map(|(_, v)| v);
123 let dtype = match field("dtype") {
124 Some(Json::Str(d)) => {
125 dtype_of(d).ok_or_else(|| invalid(format!("`{name}`: unsupported dtype {d}")))?
126 }
127 _ => return Err(invalid(format!("`{name}`: missing dtype"))),
128 };
129 let shape = match field("shape") {
130 Some(Json::Array(dims)) => dims
131 .iter()
132 .map(|d| usize_of(d, "a dimension"))
133 .collect::<Result<Vec<_>>>()?,
134 _ => return Err(invalid(format!("`{name}`: missing shape"))),
135 };
136 let (begin, end) = match field("data_offsets") {
137 Some(Json::Array(o)) if o.len() == 2 => {
138 (usize_of(&o[0], "an offset")?, usize_of(&o[1], "an offset")?)
139 }
140 _ => return Err(invalid(format!("`{name}`: missing data_offsets"))),
141 };
142 let expected = shape
143 .iter()
144 .try_fold(dtype.size(), |n, &d| n.checked_mul(d))
145 .ok_or_else(|| invalid(format!("`{name}`: size overflows")))?;
146 if end.checked_sub(begin) != Some(expected) {
147 return Err(invalid(format!(
148 "`{name}`: data_offsets [{begin}, {end}] do not hold {expected} bytes"
149 )));
150 }
151 ranges.push((begin, end, name, dtype, shape));
152 }
153 ranges.sort_by_key(|r| (r.0, r.1));
155 let mut at = 0;
156 for (begin, end, name, ..) in &ranges {
157 if *begin != at {
158 return Err(invalid(format!(
159 "`{name}`: data_offsets overlap or leave a gap"
160 )));
161 }
162 at = *end;
163 }
164 if start + at != bytes.len() {
165 return Err(invalid(
166 "the tensors do not cover the data to the end of the file",
167 ));
168 }
169 let aligned = ranges
173 .iter()
174 .all(|(begin, _, _, dtype, _)| (start + begin).is_multiple_of(dtype.size()));
175 let (data, base) = match aligned {
176 true => (file, start),
177 false => (file.section(start, at)?, 0),
178 };
179 for (begin, _, name, dtype, shape) in ranges {
180 out.tensors
181 .insert(name, data.array(base + begin, dtype, &shape)?);
182 }
183 Ok(out)
184}
185
186pub(crate) fn array_bytes(a: &Array) -> Result<Vec<u8>> {
188 macro_rules! le {
189 ($t:ty) => {
190 a.to_vec::<$t>()?
191 .iter()
192 .flat_map(|v| v.to_le_bytes())
193 .collect()
194 };
195 }
196 Ok(match a.dtype() {
197 DType::Bool => a.to_vec::<bool>()?.iter().map(|&b| u8::from(b)).collect(),
198 DType::U8 => a.to_vec::<u8>()?,
199 DType::U16 => le!(u16),
200 DType::U32 => le!(u32),
201 DType::U64 => le!(u64),
202 DType::I8 => le!(i8),
203 DType::I16 => le!(i16),
204 DType::I32 => le!(i32),
205 DType::I64 => le!(i64),
206 DType::F16 => le!(nuvec_core::f16),
207 DType::BF16 => le!(nuvec_core::bf16),
208 DType::F32 => le!(f32),
209 DType::F64 => le!(f64),
210 _ => a
211 .to_vec::<nuvec_core::Complex64>()?
212 .iter()
213 .flat_map(|z| [z.re.to_le_bytes(), z.im.to_le_bytes()].concat())
214 .collect(),
215 })
216}
217
218pub fn save_safetensors(
224 path: impl AsRef<Path>,
225 tensors: &[(&str, &Array)],
226 metadata: &[(&str, &str)],
227) -> Result<()> {
228 let mut sorted: Vec<&(&str, &Array)> = tensors.iter().collect();
229 sorted.sort_by(|(ln, la), (rn, ra)| ra.dtype().size().cmp(&la.dtype().size()).then(ln.cmp(rn)));
230 let mut header = Vec::new();
231 if !metadata.is_empty() {
232 let map: BTreeMap<String, String> = metadata
233 .iter()
234 .map(|(k, v)| (k.to_string(), v.to_string()))
235 .collect();
236 header.push(format!("\"__metadata__\":{}", json::object(&map)));
237 }
238 let mut data = Vec::new();
239 for (name, a) in sorted {
240 let bytes = array_bytes(a)?;
241 let shape: Vec<String> = a.shape().iter().map(usize::to_string).collect();
242 header.push(format!(
243 "{}:{{\"dtype\":\"{}\",\"shape\":[{}],\"data_offsets\":[{},{}]}}",
244 json::quote(name),
245 name_of(a.dtype()),
246 shape.join(","),
247 data.len(),
248 data.len() + bytes.len()
249 ));
250 data.extend(bytes);
251 }
252 let mut header = format!("{{{}}}", header.join(",")).into_bytes();
253 header.resize(header.len().next_multiple_of(8), b' ');
254 let mut out = Vec::with_capacity(8 + header.len() + data.len());
255 out.extend((header.len() as u64).to_le_bytes());
256 out.extend(header);
257 out.extend(data);
258 std::fs::write(path.as_ref(), out)
259 .map_err(|e| Error::Io(format!("{}: {e}", path.as_ref().display())))
260}