1use std::collections::{BTreeMap, BTreeSet};
11use std::path::Path;
12
13use nuvec_core::{Array, DType, Error, MappedFile, Result};
14
15use crate::json::{self, Json};
16
17fn invalid(msg: impl Into<String>) -> Error {
18 Error::InvalidArgument(format!("safetensors: {}", msg.into()))
19}
20
21#[derive(Debug, Clone, Default)]
23pub struct SafeTensors {
24 pub tensors: BTreeMap<String, Array>,
26 pub metadata: BTreeMap<String, String>,
28}
29
30fn dtype_of(name: &str) -> Option<DType> {
32 Some(match name {
33 "BOOL" => DType::Bool,
34 "U8" => DType::U8,
35 "U16" => DType::U16,
36 "U32" => DType::U32,
37 "U64" => DType::U64,
38 "I8" => DType::I8,
39 "I16" => DType::I16,
40 "I32" => DType::I32,
41 "I64" => DType::I64,
42 "F16" => DType::F16,
43 "BF16" => DType::BF16,
44 "F32" => DType::F32,
45 "F64" => DType::F64,
46 "C64" => DType::Complex64,
47 _ => return None,
48 })
49}
50
51fn name_of(dtype: DType) -> &'static str {
52 match dtype {
53 DType::Bool => "BOOL",
54 DType::U8 => "U8",
55 DType::U16 => "U16",
56 DType::U32 => "U32",
57 DType::U64 => "U64",
58 DType::I8 => "I8",
59 DType::I16 => "I16",
60 DType::I32 => "I32",
61 DType::I64 => "I64",
62 DType::F16 => "F16",
63 DType::BF16 => "BF16",
64 DType::F32 => "F32",
65 DType::F64 => "F64",
66 _ => "C64",
67 }
68}
69
70fn usize_of(v: &Json, what: &str) -> Result<usize> {
71 match v {
72 Json::Int(n) if *n >= 0 => Ok(*n as usize),
73 _ => Err(invalid(format!("{what} must be a non-negative integer"))),
74 }
75}
76
77const MAX_HEADER: usize = 100_000_000;
79
80struct Header {
83 metadata: BTreeMap<String, String>,
84 ranges: Vec<(usize, usize, String, DType, Vec<usize>)>,
86 start: usize,
88}
89
90impl Header {
91 fn aligned(&self) -> bool {
94 self.ranges
95 .iter()
96 .all(|(begin, _, _, dtype, _)| (self.start + begin).is_multiple_of(dtype.size()))
97 }
98
99 fn plan(&self, keep: &dyn Fn(&str) -> bool) -> ReadPlan {
105 let mut plan = ReadPlan::default();
106 let mut file_end = None;
108 for (index, (begin, end, name, dtype, _)) in self.ranges.iter().enumerate() {
109 if !keep(name) {
110 continue;
111 }
112 let at = (self.start + begin) as u64;
113 let n = end - begin;
114 let offset = if file_end == Some(at) && plan.len.is_multiple_of(dtype.size()) {
115 let last = plan.ranges.last_mut().expect("an open range");
116 last.2 += n;
117 plan.len
118 } else {
119 let offset = plan.len.next_multiple_of(16);
120 plan.ranges.push((at, offset, n));
121 offset
122 };
123 plan.tensors.push((index, offset));
124 plan.len = offset + n;
125 file_end = Some(at + n as u64);
126 }
127 plan
128 }
129}
130
131#[derive(Default)]
133struct ReadPlan {
134 ranges: Vec<(u64, usize, usize)>,
136 tensors: Vec<(usize, usize)>,
138 len: usize,
140}
141
142fn header_end(size: [u8; 8], file_len: usize) -> Result<usize> {
144 usize::try_from(u64::from_le_bytes(size))
145 .ok()
146 .filter(|&n| n <= MAX_HEADER)
147 .and_then(|n| n.checked_add(8))
148 .filter(|&end| end <= file_len)
149 .ok_or_else(|| invalid("header larger than the file or 100 MB"))
150}
151
152fn parse_header(json_bytes: &[u8], start: usize, file_len: usize) -> Result<Header> {
155 let Json::Object(entries) = json::parse(json_bytes)? else {
156 return Err(invalid("the header must be a JSON object"));
157 };
158 let mut metadata = BTreeMap::new();
159 let mut ranges = Vec::new();
160 let mut names = BTreeSet::new();
161 for (name, entry) in entries {
162 if name == "__metadata__" {
163 match entry {
164 Json::Null => {}
165 Json::Object(fields) => {
166 for (k, v) in fields {
167 let Json::Str(s) = v else {
168 return Err(invalid("metadata values must be strings"));
169 };
170 metadata.insert(k, s);
171 }
172 }
173 _ => return Err(invalid("`__metadata__` must be an object")),
174 }
175 continue;
176 }
177 if !names.insert(name.clone()) {
179 return Err(invalid(format!("duplicate tensor `{name}`")));
180 }
181 let Json::Object(fields) = entry else {
182 return Err(invalid(format!("entry `{name}` must be an object")));
183 };
184 let field = |key: &str| fields.iter().find(|(k, _)| k == key).map(|(_, v)| v);
185 let dtype = match field("dtype") {
186 Some(Json::Str(d)) => {
187 dtype_of(d).ok_or_else(|| invalid(format!("`{name}`: unsupported dtype {d}")))?
188 }
189 _ => return Err(invalid(format!("`{name}`: missing dtype"))),
190 };
191 let shape = match field("shape") {
192 Some(Json::Array(dims)) => dims
193 .iter()
194 .map(|d| usize_of(d, "a dimension"))
195 .collect::<Result<Vec<_>>>()?,
196 _ => return Err(invalid(format!("`{name}`: missing shape"))),
197 };
198 let (begin, end) = match field("data_offsets") {
199 Some(Json::Array(o)) if o.len() == 2 => {
200 (usize_of(&o[0], "an offset")?, usize_of(&o[1], "an offset")?)
201 }
202 _ => return Err(invalid(format!("`{name}`: missing data_offsets"))),
203 };
204 let expected = shape
205 .iter()
206 .try_fold(dtype.size(), |n, &d| n.checked_mul(d))
207 .ok_or_else(|| invalid(format!("`{name}`: size overflows")))?;
208 if end.checked_sub(begin) != Some(expected) {
209 return Err(invalid(format!(
210 "`{name}`: data_offsets [{begin}, {end}] do not hold {expected} bytes"
211 )));
212 }
213 ranges.push((begin, end, name, dtype, shape));
214 }
215 ranges.sort_by_key(|r| (r.0, r.1));
217 let mut at = 0;
218 for (begin, end, name, ..) in &ranges {
219 if *begin != at {
220 return Err(invalid(format!(
221 "`{name}`: data_offsets overlap or leave a gap"
222 )));
223 }
224 at = *end;
225 }
226 if start.checked_add(at) != Some(file_len) {
227 return Err(invalid(
228 "the tensors do not cover the data to the end of the file",
229 ));
230 }
231 Ok(Header {
232 metadata,
233 ranges,
234 start,
235 })
236}
237
238fn read_header(path: &Path) -> Result<(Header, usize)> {
241 use std::io::Read;
242 let io = |e: std::io::Error| Error::Io(format!("{}: {e}", path.display()));
243 let mut file = std::fs::File::open(path).map_err(io)?;
244 let file_len = usize::try_from(file.metadata().map_err(io)?.len())
245 .map_err(|_| Error::Io(format!("{}: too large", path.display())))?;
246 let mut size = [0u8; 8];
247 file.read_exact(&mut size).map_err(|_| {
248 invalid(format!(
249 "{}: file shorter than its header size",
250 path.display()
251 ))
252 })?;
253 let start = header_end(size, file_len).map_err(|e| named(path, e))?;
254 let mut json_bytes = vec![0u8; start - 8];
255 file.read_exact(&mut json_bytes).map_err(io)?;
256 let header = parse_header(&json_bytes, start, file_len).map_err(|e| named(path, e))?;
257 Ok((header, file_len))
258}
259
260fn named(path: &Path, e: Error) -> Error {
262 match e {
263 Error::InvalidArgument(m) => Error::InvalidArgument(format!("{}: {m}", path.display())),
264 Error::Io(m) => Error::Io(format!("{}: {m}", path.display())),
265 e => e,
266 }
267}
268
269#[non_exhaustive]
273#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
274pub struct Footprint {
275 pub mapped: u64,
277 pub copied: u64,
281 pub transient: u64,
284}
285
286impl Footprint {
287 fn of(header: &Header, file_len: usize) -> Self {
289 if header.aligned() {
290 let mapping = nuvec_core::MappedFile::mapping_size(file_len as u64);
291 Footprint {
292 mapped: mapping,
293 copied: 0,
294 transient: mapping,
295 }
296 } else {
297 let copied = nuvec_core::MappedFile::copy_size(header.plan(&|_| true).len as u64);
298 Footprint {
299 mapped: 0,
300 copied,
301 transient: copied,
302 }
303 }
304 }
305}
306
307pub fn safetensors_footprint(path: impl AsRef<Path>) -> Result<Footprint> {
330 let path = path.as_ref();
331 let files = if path.is_dir() {
332 let mut files: Vec<_> = std::fs::read_dir(path)
333 .map_err(|e| Error::Io(format!("{}: {e}", path.display())))?
334 .filter_map(|e| e.ok().map(|e| e.path()))
335 .filter(|p| p.extension().is_some_and(|x| x == "safetensors"))
336 .collect();
337 files.sort();
338 files
339 } else {
340 vec![path.to_path_buf()]
341 };
342 let mut total = Footprint::default();
343 for file in &files {
344 let (header, len) = read_header(file)?;
345 let one = Footprint::of(&header, len);
346 total.mapped += one.mapped;
347 total.copied += one.copied;
348 }
349 total.transient = total.mapped + total.copied;
350 Ok(total)
351}
352
353pub fn load_safetensors(path: impl AsRef<Path>) -> Result<SafeTensors> {
365 load_safetensors_filtered(path, |_| true)
366}
367
368pub fn load_safetensors_filtered(
386 path: impl AsRef<Path>,
387 keep: impl Fn(&str) -> bool,
388) -> Result<SafeTensors> {
389 let path = path.as_ref();
390 let (header, len) = read_header(path)?;
391 let aligned = header.aligned();
392 let plan = (!aligned).then(|| header.plan(&keep));
393 let need = match &plan {
394 Some(plan) => nuvec_core::MappedFile::copy_size(plan.len as u64),
395 None => Footprint::of(&header, len).transient,
396 };
397 if let Some(room) = nuvec_core::MappedFile::headroom()
398 && need > room
399 {
400 return Err(Error::Io(format!(
401 "{}: out of memory: loading needs {need} bytes (mapped or read), {room} \
402 are left under the memory limit",
403 path.display()
404 )));
405 }
406 let oom = |e| match e {
407 Error::OutOfMemory(n) => Error::Io(format!(
408 "{}: out of memory: {n} bytes pass the memory limit",
409 path.display()
410 )),
411 e => e,
412 };
413 let mut out = SafeTensors {
414 metadata: header.metadata,
415 ..SafeTensors::default()
416 };
417 match plan {
418 Some(plan) => {
420 let data = MappedFile::read_ranges(path, &plan.ranges, plan.len).map_err(oom)?;
421 let mut ranges: Vec<_> = header.ranges.into_iter().map(Some).collect();
422 for (index, offset) in plan.tensors {
423 let (_, _, name, dtype, shape) = ranges[index].take().expect("kept once");
424 out.tensors.insert(name, data.array(offset, dtype, &shape)?);
425 }
426 }
427 None => {
428 let file = MappedFile::open(path).map_err(oom)?;
429 if file.len() != len {
432 return Err(Error::Io(format!(
433 "{}: the file changed while it was loaded",
434 path.display()
435 )));
436 }
437 for (begin, _, name, dtype, shape) in header.ranges {
438 if keep(&name) {
439 let tensor = file.array(header.start + begin, dtype, &shape)?;
440 out.tensors.insert(name, tensor);
441 }
442 }
443 }
444 }
445 Ok(out)
446}
447
448pub(crate) fn array_bytes(a: &Array) -> Result<Vec<u8>> {
450 macro_rules! le {
451 ($t:ty) => {
452 a.to_vec::<$t>()?
453 .iter()
454 .flat_map(|v| v.to_le_bytes())
455 .collect()
456 };
457 }
458 Ok(match a.dtype() {
459 DType::Bool => a.to_vec::<bool>()?.iter().map(|&b| u8::from(b)).collect(),
460 DType::U8 => a.to_vec::<u8>()?,
461 DType::U16 => le!(u16),
462 DType::U32 => le!(u32),
463 DType::U64 => le!(u64),
464 DType::I8 => le!(i8),
465 DType::I16 => le!(i16),
466 DType::I32 => le!(i32),
467 DType::I64 => le!(i64),
468 DType::F16 => le!(nuvec_core::f16),
469 DType::BF16 => le!(nuvec_core::bf16),
470 DType::F32 => le!(f32),
471 DType::F64 => le!(f64),
472 _ => a
473 .to_vec::<nuvec_core::Complex64>()?
474 .iter()
475 .flat_map(|z| [z.re.to_le_bytes(), z.im.to_le_bytes()].concat())
476 .collect(),
477 })
478}
479
480fn encode(tensors: &[(&str, &Array)], metadata: &[(&str, &str)]) -> Result<Vec<u8>> {
482 let mut sorted: Vec<&(&str, &Array)> = tensors.iter().collect();
483 sorted.sort_by(|(ln, la), (rn, ra)| ra.dtype().size().cmp(&la.dtype().size()).then(ln.cmp(rn)));
484 let mut header = Vec::new();
485 if !metadata.is_empty() {
486 let map: BTreeMap<String, String> = metadata
487 .iter()
488 .map(|(k, v)| (k.to_string(), v.to_string()))
489 .collect();
490 header.push(format!("\"__metadata__\":{}", json::object(&map)));
491 }
492 let mut data = Vec::new();
493 for (name, a) in sorted {
494 let bytes = array_bytes(a)?;
495 let shape: Vec<String> = a.shape().iter().map(i32::to_string).collect();
496 header.push(format!(
497 "{}:{{\"dtype\":\"{}\",\"shape\":[{}],\"data_offsets\":[{},{}]}}",
498 json::quote(name),
499 name_of(a.dtype()),
500 shape.join(","),
501 data.len(),
502 data.len() + bytes.len()
503 ));
504 data.extend(bytes);
505 }
506 let mut header = format!("{{{}}}", header.join(",")).into_bytes();
507 header.resize(header.len().next_multiple_of(8), b' ');
508 let mut out = Vec::with_capacity(8 + header.len() + data.len());
509 out.extend((header.len() as u64).to_le_bytes());
510 out.extend(header);
511 out.extend(data);
512 Ok(out)
513}
514
515fn write_atomically(path: &Path, bytes: &[u8]) -> Result<()> {
517 let io = |e: std::io::Error| Error::Io(format!("{}: {e}", path.display()));
518 let mut tmp = path.as_os_str().to_owned();
519 tmp.push(".tmp");
520 let tmp = std::path::PathBuf::from(tmp);
521 if let Err(e) = std::fs::write(&tmp, bytes) {
522 let _ = std::fs::remove_file(&tmp);
523 return Err(io(e));
524 }
525 std::fs::rename(&tmp, path).map_err(|e| {
526 let _ = std::fs::remove_file(&tmp);
527 io(e)
528 })
529}
530
531pub fn save_safetensors(
538 path: impl AsRef<Path>,
539 tensors: &[(&str, &Array)],
540 metadata: &[(&str, &str)],
541) -> Result<()> {
542 write_atomically(path.as_ref(), &encode(tensors, metadata)?)
543}
544
545#[must_use = "wait() returns the write's result; dropping the handle only waits"]
548#[derive(Debug)]
549pub struct SaveHandle {
550 thread: Option<std::thread::JoinHandle<Result<()>>>,
551}
552
553impl SaveHandle {
554 pub fn wait(mut self) -> Result<()> {
556 self.join()
557 }
558
559 fn join(&mut self) -> Result<()> {
560 match self.thread.take() {
561 Some(t) => t
562 .join()
563 .unwrap_or_else(|_| Err(Error::Io("the safetensors writer panicked".into()))),
564 None => Ok(()),
565 }
566 }
567}
568
569impl Drop for SaveHandle {
570 fn drop(&mut self) {
571 let _ = self.join();
572 }
573}
574
575pub fn save_safetensors_async(
598 path: impl AsRef<Path>,
599 tensors: &[(&str, &Array)],
600 metadata: &[(&str, &str)],
601) -> SaveHandle {
602 let path = path.as_ref().to_path_buf();
603 let tensors: Vec<(String, Array)> = tensors
604 .iter()
605 .map(|(n, a)| ((*n).to_string(), (*a).clone()))
606 .collect();
607 let metadata: Vec<(String, String)> = metadata
608 .iter()
609 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
610 .collect();
611 let submitted = nuvec_core::async_eval(&tensors.iter().map(|(_, a)| a).collect::<Vec<_>>());
612 let thread = std::thread::spawn(move || {
613 submitted?;
614 let tensors: Vec<(&str, &Array)> = tensors.iter().map(|(n, a)| (n.as_str(), a)).collect();
615 let metadata: Vec<(&str, &str)> = metadata
616 .iter()
617 .map(|(k, v)| (k.as_str(), v.as_str()))
618 .collect();
619 write_atomically(&path, &encode(&tensors, &metadata)?)
620 });
621 SaveHandle {
622 thread: Some(thread),
623 }
624}