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 "F8_E4M3" | "F8_E8M0" => DType::U8,
50 _ => return None,
51 })
52}
53
54fn name_of(dtype: DType) -> &'static str {
55 match dtype {
56 DType::Bool => "BOOL",
57 DType::U8 => "U8",
58 DType::U16 => "U16",
59 DType::U32 => "U32",
60 DType::U64 => "U64",
61 DType::I8 => "I8",
62 DType::I16 => "I16",
63 DType::I32 => "I32",
64 DType::I64 => "I64",
65 DType::F16 => "F16",
66 DType::BF16 => "BF16",
67 DType::F32 => "F32",
68 DType::F64 => "F64",
69 _ => "C64",
70 }
71}
72
73fn usize_of(v: &Json, what: &str) -> Result<usize> {
74 match v {
75 Json::Int(n) if *n >= 0 => Ok(*n as usize),
76 _ => Err(invalid(format!("{what} must be a non-negative integer"))),
77 }
78}
79
80const MAX_HEADER: usize = 100_000_000;
82
83const TENSOR_ALIGN: usize = 256;
86
87struct Header {
90 metadata: BTreeMap<String, String>,
91 ranges: Vec<(usize, usize, String, DType, Vec<usize>)>,
93 start: usize,
95}
96
97impl Header {
98 fn aligned(&self) -> bool {
101 self.ranges
102 .iter()
103 .all(|(begin, _, _, dtype, _)| (self.start + begin).is_multiple_of(dtype.size()))
104 }
105
106 fn plan(&self, keep: &dyn Fn(&str) -> bool) -> ReadPlan {
113 let mut plan = ReadPlan::default();
114 let mut file_end = None;
116 for (index, (begin, end, name, _, _)) in self.ranges.iter().enumerate() {
117 if !keep(name) {
118 continue;
119 }
120 let at = (self.start + begin) as u64;
121 let n = end - begin;
122 let offset = if file_end == Some(at) && plan.len.is_multiple_of(TENSOR_ALIGN) {
123 let last = plan.ranges.last_mut().expect("an open range");
124 last.2 += n;
125 plan.len
126 } else {
127 let offset = plan.len.next_multiple_of(TENSOR_ALIGN);
128 plan.ranges.push((at, offset, n));
129 offset
130 };
131 plan.tensors.push((index, offset));
132 plan.len = offset + n;
133 file_end = Some(at + n as u64);
134 }
135 plan
136 }
137}
138
139impl Header {
140 fn runs(&self, file_len: usize, page: u64, keep: &dyn Fn(&str) -> bool) -> Vec<(u64, u64)> {
145 let up = |x: u64| x.div_ceil(page) * page;
146 if self.ranges.iter().all(|(_, _, name, _, _)| keep(name)) {
147 return vec![(0, up(file_len as u64))];
148 }
149 let mut runs: Vec<(u64, u64)> = Vec::new();
150 for (begin, end, name, _, _) in &self.ranges {
151 if begin == end || !keep(name) {
152 continue;
153 }
154 let lo = (self.start + begin) as u64 / page * page;
155 let hi = up((self.start + end) as u64);
156 match runs.last_mut() {
157 Some((at, len)) if lo <= *at + *len => *len = hi.max(*at + *len) - *at,
158 _ => runs.push((lo, hi - lo)),
159 }
160 }
161 runs
162 }
163}
164
165#[derive(Default)]
167struct ReadPlan {
168 ranges: Vec<(u64, usize, usize)>,
170 tensors: Vec<(usize, usize)>,
172 len: usize,
174}
175
176fn header_end(size: [u8; 8], file_len: usize) -> Result<usize> {
178 usize::try_from(u64::from_le_bytes(size))
179 .ok()
180 .filter(|&n| n <= MAX_HEADER)
181 .and_then(|n| n.checked_add(8))
182 .filter(|&end| end <= file_len)
183 .ok_or_else(|| invalid("header larger than the file or 100 MB"))
184}
185
186fn parse_header(json_bytes: &[u8], start: usize, file_len: usize) -> Result<Header> {
189 let Json::Object(entries) = json::parse(json_bytes)? else {
190 return Err(invalid("the header must be a JSON object"));
191 };
192 let mut metadata = BTreeMap::new();
193 let mut ranges = Vec::new();
194 let mut names = BTreeSet::new();
195 for (name, entry) in entries {
196 if name == "__metadata__" {
197 match entry {
198 Json::Null => {}
199 Json::Object(fields) => {
200 for (k, v) in fields {
201 let Json::Str(s) = v else {
202 return Err(invalid("metadata values must be strings"));
203 };
204 metadata.insert(k, s);
205 }
206 }
207 _ => return Err(invalid("`__metadata__` must be an object")),
208 }
209 continue;
210 }
211 if !names.insert(name.clone()) {
213 return Err(invalid(format!("duplicate tensor `{name}`")));
214 }
215 let Json::Object(fields) = entry else {
216 return Err(invalid(format!("entry `{name}` must be an object")));
217 };
218 let field = |key: &str| fields.iter().find(|(k, _)| k == key).map(|(_, v)| v);
219 let dtype = match field("dtype") {
220 Some(Json::Str(d)) => {
221 dtype_of(d).ok_or_else(|| invalid(format!("`{name}`: unsupported dtype {d}")))?
222 }
223 _ => return Err(invalid(format!("`{name}`: missing dtype"))),
224 };
225 let shape = match field("shape") {
226 Some(Json::Array(dims)) => dims
227 .iter()
228 .map(|d| usize_of(d, "a dimension"))
229 .collect::<Result<Vec<_>>>()?,
230 _ => return Err(invalid(format!("`{name}`: missing shape"))),
231 };
232 let (begin, end) = match field("data_offsets") {
233 Some(Json::Array(o)) if o.len() == 2 => {
234 (usize_of(&o[0], "an offset")?, usize_of(&o[1], "an offset")?)
235 }
236 _ => return Err(invalid(format!("`{name}`: missing data_offsets"))),
237 };
238 let expected = shape
239 .iter()
240 .try_fold(dtype.size(), |n, &d| n.checked_mul(d))
241 .ok_or_else(|| invalid(format!("`{name}`: size overflows")))?;
242 if end.checked_sub(begin) != Some(expected) {
243 return Err(invalid(format!(
244 "`{name}`: data_offsets [{begin}, {end}] do not hold {expected} bytes"
245 )));
246 }
247 ranges.push((begin, end, name, dtype, shape));
248 }
249 ranges.sort_by_key(|r| (r.0, r.1));
251 let mut at = 0;
252 for (begin, end, name, ..) in &ranges {
253 if *begin != at {
254 return Err(invalid(format!(
255 "`{name}`: data_offsets overlap or leave a gap"
256 )));
257 }
258 at = *end;
259 }
260 if start.checked_add(at) != Some(file_len) {
261 return Err(invalid(
262 "the tensors do not cover the data to the end of the file",
263 ));
264 }
265 Ok(Header {
266 metadata,
267 ranges,
268 start,
269 })
270}
271
272fn read_header(path: &Path) -> Result<(Header, usize)> {
275 use std::io::Read;
276 let io = |e: std::io::Error| Error::Io(format!("{}: {e}", path.display()));
277 let mut file = std::fs::File::open(path).map_err(io)?;
278 let file_len = usize::try_from(file.metadata().map_err(io)?.len())
279 .map_err(|_| Error::Io(format!("{}: too large", path.display())))?;
280 let mut size = [0u8; 8];
281 file.read_exact(&mut size).map_err(|_| {
282 invalid(format!(
283 "{}: file shorter than its header size",
284 path.display()
285 ))
286 })?;
287 let start = header_end(size, file_len).map_err(|e| named(path, e))?;
288 let mut json_bytes = vec![0u8; start - 8];
289 file.read_exact(&mut json_bytes).map_err(io)?;
290 let header = parse_header(&json_bytes, start, file_len).map_err(|e| named(path, e))?;
291 Ok((header, file_len))
292}
293
294fn named(path: &Path, e: Error) -> Error {
296 match e {
297 Error::InvalidArgument(m) => Error::InvalidArgument(format!("{}: {m}", path.display())),
298 Error::Io(m) => Error::Io(format!("{}: {m}", path.display())),
299 e => e,
300 }
301}
302
303#[non_exhaustive]
307#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
308pub struct Footprint {
309 pub mapped: u64,
312 pub copied: u64,
316 pub transient: u64,
319}
320
321impl Footprint {
322 fn of(header: &Header, file_len: usize, keep: &dyn Fn(&str) -> bool) -> Self {
325 if header.aligned() {
326 let page = nuvec_core::MappedRanges::page_size() as u64;
327 let mapping = header.runs(file_len, page, keep).iter().map(|r| r.1).sum();
328 Footprint {
329 mapped: mapping,
330 copied: 0,
331 transient: mapping,
332 }
333 } else {
334 let copied = nuvec_core::MappedFile::copy_size(header.plan(keep).len as u64);
335 Footprint {
336 mapped: 0,
337 copied,
338 transient: copied,
339 }
340 }
341 }
342}
343
344pub fn safetensors_footprint(path: impl AsRef<Path>) -> Result<Footprint> {
367 safetensors_footprint_filtered(path, |_| true)
368}
369
370pub fn safetensors_footprint_filtered(
388 path: impl AsRef<Path>,
389 keep: impl Fn(&str) -> bool,
390) -> Result<Footprint> {
391 let keep = |name: &str| !name.starts_with(PAD) && keep(name);
392 let path = path.as_ref();
393 let files = if path.is_dir() {
394 let mut files: Vec<_> = std::fs::read_dir(path)
395 .map_err(|e| Error::Io(format!("{}: {e}", path.display())))?
396 .filter_map(|e| e.ok().map(|e| e.path()))
397 .filter(|p| p.extension().is_some_and(|x| x == "safetensors"))
398 .collect();
399 files.sort();
400 files
401 } else {
402 vec![path.to_path_buf()]
403 };
404 let mut total = Footprint::default();
405 for file in &files {
406 let (header, len) = read_header(file)?;
407 let one = Footprint::of(&header, len, &keep);
408 total.mapped += one.mapped;
409 total.copied += one.copied;
410 }
411 total.transient = total.mapped + total.copied;
412 Ok(total)
413}
414
415#[derive(Debug, Clone, PartialEq, Eq)]
417#[non_exhaustive]
418pub struct TensorInfo {
419 pub name: String,
421 pub dtype: DType,
423 pub shape: Vec<usize>,
425 pub bytes: usize,
427}
428
429pub fn safetensors_tensors(path: impl AsRef<Path>) -> Result<Vec<TensorInfo>> {
445 let path = path.as_ref();
446 let (header, _) = read_header(path)?;
447 Ok(header
448 .ranges
449 .into_iter()
450 .filter(|(_, _, name, _, _)| !name.starts_with(PAD))
451 .map(|(begin, end, name, dtype, shape)| TensorInfo {
452 name,
453 dtype,
454 shape,
455 bytes: end - begin,
456 })
457 .collect())
458}
459
460pub fn load_safetensors(path: impl AsRef<Path>) -> Result<SafeTensors> {
472 load_safetensors_filtered(path, |_| true)
473}
474
475pub fn load_safetensors_filtered(
493 path: impl AsRef<Path>,
494 keep: impl Fn(&str) -> bool,
495) -> Result<SafeTensors> {
496 let keep = |name: &str| !name.starts_with(PAD) && keep(name);
497 let path = path.as_ref();
498 let (header, len) = read_header(path)?;
499 let aligned = header.aligned();
500 let plan = (!aligned).then(|| header.plan(&keep));
501 let page = nuvec_core::MappedRanges::page_size() as u64;
502 let runs = header.runs(len, page, &keep);
503 let need = match &plan {
504 Some(plan) => nuvec_core::MappedFile::copy_size(plan.len as u64),
505 None => runs.iter().map(|r| r.1).sum(),
506 };
507 if let Some(room) = nuvec_core::MappedFile::headroom()
508 && need > room
509 {
510 return Err(Error::Io(format!(
511 "{}: out of memory: loading needs {need} bytes (mapped or read), {room} \
512 are left under the memory limit",
513 path.display()
514 )));
515 }
516 let oom = |e| match e {
517 Error::OutOfMemory(n) => Error::Io(format!(
518 "{}: out of memory: {n} bytes pass the memory limit",
519 path.display()
520 )),
521 e => e,
522 };
523 let mut out = SafeTensors {
524 metadata: header.metadata,
525 ..SafeTensors::default()
526 };
527 match plan {
528 Some(plan) => {
530 let data = MappedFile::read_ranges(path, &plan.ranges, plan.len).map_err(oom)?;
531 let mut ranges: Vec<_> = header.ranges.into_iter().map(Some).collect();
532 for (index, offset) in plan.tensors {
533 let (_, _, name, dtype, shape) = ranges[index].take().expect("kept once");
534 out.tensors.insert(name, data.array(offset, dtype, &shape)?);
535 }
536 }
537 None if runs != [(0, len.div_ceil(page as usize) as u64 * page)] => {
539 let ranges: Vec<(u64, u64)> = runs
542 .iter()
543 .map(|&(o, n)| (o, n.min(len as u64 - o)))
544 .collect();
545 let file = nuvec_core::MappedRanges::open(path, &ranges).map_err(oom)?;
546 for (begin, end, name, dtype, shape) in header.ranges {
547 if keep(&name) {
548 let tensor = if begin == end {
549 nuvec_core::ops::zeros(
550 shape.iter().map(|&d| d as i32).collect::<Vec<_>>(),
551 dtype,
552 )
553 } else {
554 file.array(header.start + begin, dtype, &shape)?
555 };
556 out.tensors.insert(name, tensor);
557 }
558 }
559 }
560 None => {
561 let file = MappedFile::open(path).map_err(oom)?;
562 if file.len() != len {
565 return Err(Error::Io(format!(
566 "{}: the file changed while it was loaded",
567 path.display()
568 )));
569 }
570 for (begin, _, name, dtype, shape) in header.ranges {
571 if keep(&name) {
572 let tensor = file.array(header.start + begin, dtype, &shape)?;
573 out.tensors.insert(name, tensor);
574 }
575 }
576 }
577 }
578 Ok(out)
579}
580
581pub(crate) fn array_bytes(a: &Array) -> Result<Vec<u8>> {
583 macro_rules! le {
584 ($t:ty) => {
585 a.to_vec::<$t>()?
586 .iter()
587 .flat_map(|v| v.to_le_bytes())
588 .collect()
589 };
590 }
591 Ok(match a.dtype() {
592 DType::Bool => a.to_vec::<bool>()?.iter().map(|&b| u8::from(b)).collect(),
593 DType::U8 => a.to_vec::<u8>()?,
594 DType::U16 => le!(u16),
595 DType::U32 => le!(u32),
596 DType::U64 => le!(u64),
597 DType::I8 => le!(i8),
598 DType::I16 => le!(i16),
599 DType::I32 => le!(i32),
600 DType::I64 => le!(i64),
601 DType::F16 => le!(nuvec_core::f16),
602 DType::BF16 => le!(nuvec_core::bf16),
603 DType::F32 => le!(f32),
604 DType::F64 => le!(f64),
605 _ => a
606 .to_vec::<nuvec_core::Complex64>()?
607 .iter()
608 .flat_map(|z| [z.re.to_le_bytes(), z.im.to_le_bytes()].concat())
609 .collect(),
610 })
611}
612
613fn encode(tensors: &[(&str, &Array)], metadata: &[(&str, &str)]) -> Result<Vec<u8>> {
615 let mut sorted: Vec<&(&str, &Array)> = tensors.iter().collect();
616 sorted.sort_by(|(ln, la), (rn, ra)| ra.dtype().size().cmp(&la.dtype().size()).then(ln.cmp(rn)));
617 let mut header = Vec::new();
618 if !metadata.is_empty() {
619 let map: BTreeMap<String, String> = metadata
620 .iter()
621 .map(|(k, v)| (k.to_string(), v.to_string()))
622 .collect();
623 header.push(format!("\"__metadata__\":{}", json::object(&map)));
624 }
625 let mut data = Vec::new();
626 for (name, a) in sorted {
627 let bytes = array_bytes(a)?;
628 let shape: Vec<String> = a.shape().iter().map(i32::to_string).collect();
629 header.push(format!(
630 "{}:{{\"dtype\":\"{}\",\"shape\":[{}],\"data_offsets\":[{},{}]}}",
631 json::quote(name),
632 name_of(a.dtype()),
633 shape.join(","),
634 data.len(),
635 data.len() + bytes.len()
636 ));
637 data.extend(bytes);
638 }
639 let mut header = format!("{{{}}}", header.join(",")).into_bytes();
640 header.resize(header.len().next_multiple_of(8), b' ');
641 let mut out = Vec::with_capacity(8 + header.len() + data.len());
642 out.extend((header.len() as u64).to_le_bytes());
643 out.extend(header);
644 out.extend(data);
645 Ok(out)
646}
647
648const PAD: &str = "__nuvec_pad_";
651
652pub fn align_safetensors(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
659 use std::io::Write;
660 let (src, dst) = (src.as_ref(), dst.as_ref());
661 let (header, _) = read_header(src)?;
662 let mut entries = Vec::new();
663 if !header.metadata.is_empty() {
664 entries.push(format!(
665 "\"__metadata__\":{}",
666 json::object(&header.metadata)
667 ));
668 }
669 let mut copies = Vec::new();
671 let mut at = 0usize;
672 for (i, (begin, end, name, dtype, shape)) in header.ranges.iter().enumerate() {
673 let pad = at.next_multiple_of(TENSOR_ALIGN) - at;
674 if pad > 0 {
675 entries.push(format!(
676 "\"{PAD}{i}\":{{\"dtype\":\"U8\",\"shape\":[{pad}],\"data_offsets\":[{at},{}]}}",
677 at + pad
678 ));
679 at += pad;
680 }
681 let n = end - begin;
682 let dims: Vec<String> = shape.iter().map(usize::to_string).collect();
683 entries.push(format!(
684 "{}:{{\"dtype\":\"{}\",\"shape\":[{}],\"data_offsets\":[{at},{}]}}",
685 json::quote(name),
686 name_of(*dtype),
687 dims.join(","),
688 at + n
689 ));
690 copies.push(((header.start + begin) as u64, n, at));
691 at += n;
692 }
693 let mut json = format!("{{{}}}", entries.join(",")).into_bytes();
695 json.resize((8 + json.len()).next_multiple_of(TENSOR_ALIGN) - 8, b' ');
696 fn io(p: &Path) -> impl Fn(std::io::Error) -> Error + '_ {
697 move |e| Error::Io(format!("{}: {e}", p.display()))
698 }
699 let tmp = dst.with_extension("safetensors.tmp");
700 let input = std::fs::File::open(src).map_err(io(src))?;
701 let mut out =
702 std::io::BufWriter::with_capacity(16 << 20, std::fs::File::create(&tmp).map_err(io(&tmp))?);
703 out.write_all(&(json.len() as u64).to_le_bytes())
704 .map_err(io(&tmp))?;
705 out.write_all(&json).map_err(io(&tmp))?;
706 let mut written = 0usize;
707 let mut chunk = vec![0u8; 16 << 20];
708 for (from, n, to) in copies {
709 out.write_all(&vec![0u8; to - written]).map_err(io(&tmp))?;
710 let mut done = 0;
711 while done < n {
712 let k = (n - done).min(chunk.len());
713 #[cfg(unix)]
714 std::os::unix::fs::FileExt::read_exact_at(&input, &mut chunk[..k], from + done as u64)
715 .map_err(io(src))?;
716 #[cfg(not(unix))]
717 {
718 use std::io::{Read, Seek, SeekFrom};
719 let mut f = &input;
720 f.seek(SeekFrom::Start(from + done as u64))
721 .map_err(io(src))?;
722 f.read_exact(&mut chunk[..k]).map_err(io(src))?;
723 }
724 out.write_all(&chunk[..k]).map_err(io(&tmp))?;
725 done += k;
726 }
727 written = to + n;
728 }
729 out.into_inner()
730 .map_err(|e| Error::Io(format!("{}: {e}", tmp.display())))?
731 .sync_all()
732 .map_err(io(&tmp))?;
733 std::fs::rename(&tmp, dst).map_err(io(dst))
734}
735
736pub fn load_safetensors_cached(
742 path: impl AsRef<Path>,
743 cache_dir: impl AsRef<Path>,
744 keep: impl Fn(&str) -> bool,
745) -> Result<SafeTensors> {
746 use std::hash::{Hash, Hasher};
747 let (path, cache_dir) = (path.as_ref(), cache_dir.as_ref());
748 let (header, len) = read_header(path)?;
749 if header.aligned() {
750 return load_safetensors_filtered(path, keep);
751 }
752 let meta =
753 std::fs::metadata(path).map_err(|e| Error::Io(format!("{}: {e}", path.display())))?;
754 let mtime = meta
755 .modified()
756 .ok()
757 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
758 .map_or(0, |d| d.as_nanos());
759 let mut h = std::collections::hash_map::DefaultHasher::new();
760 std::fs::canonicalize(path)
761 .unwrap_or_else(|_| path.to_path_buf())
762 .hash(&mut h);
763 let stem = path
764 .file_stem()
765 .and_then(|s| s.to_str())
766 .unwrap_or("tensors");
767 let copy = cache_dir.join(format!(
768 "{stem}-{:016x}-{len}-{mtime}.safetensors",
769 h.finish()
770 ));
771 if !copy.exists() {
772 let made = std::fs::create_dir_all(cache_dir)
773 .map_err(|e| Error::Io(format!("{}: {e}", cache_dir.display())))
774 .and_then(|()| align_safetensors(path, ©));
775 if let Err(e) = made {
776 static WARNED: std::sync::Once = std::sync::Once::new();
777 WARNED.call_once(|| {
778 eprintln!("nuvec: no aligned copy ({e}); reading {}", path.display())
779 });
780 return load_safetensors_filtered(path, keep);
781 }
782 }
783 load_safetensors_filtered(©, keep)
784}
785
786fn write_atomically(path: &Path, bytes: &[u8]) -> Result<()> {
788 let io = |e: std::io::Error| Error::Io(format!("{}: {e}", path.display()));
789 let mut tmp = path.as_os_str().to_owned();
790 tmp.push(".tmp");
791 let tmp = std::path::PathBuf::from(tmp);
792 if let Err(e) = std::fs::write(&tmp, bytes) {
793 let _ = std::fs::remove_file(&tmp);
794 return Err(io(e));
795 }
796 std::fs::rename(&tmp, path).map_err(|e| {
797 let _ = std::fs::remove_file(&tmp);
798 io(e)
799 })
800}
801
802pub fn save_safetensors(
809 path: impl AsRef<Path>,
810 tensors: &[(&str, &Array)],
811 metadata: &[(&str, &str)],
812) -> Result<()> {
813 write_atomically(path.as_ref(), &encode(tensors, metadata)?)
814}
815
816#[must_use = "wait() returns the write's result; dropping the handle only waits"]
819#[derive(Debug)]
820pub struct SaveHandle {
821 thread: Option<std::thread::JoinHandle<Result<()>>>,
822}
823
824impl SaveHandle {
825 pub fn wait(mut self) -> Result<()> {
827 self.join()
828 }
829
830 fn join(&mut self) -> Result<()> {
831 match self.thread.take() {
832 Some(t) => t
833 .join()
834 .unwrap_or_else(|_| Err(Error::Io("the safetensors writer panicked".into()))),
835 None => Ok(()),
836 }
837 }
838}
839
840impl Drop for SaveHandle {
841 fn drop(&mut self) {
842 let _ = self.join();
843 }
844}
845
846pub fn save_safetensors_async(
869 path: impl AsRef<Path>,
870 tensors: &[(&str, &Array)],
871 metadata: &[(&str, &str)],
872) -> SaveHandle {
873 let path = path.as_ref().to_path_buf();
874 let tensors: Vec<(String, Array)> = tensors
875 .iter()
876 .map(|(n, a)| ((*n).to_string(), (*a).clone()))
877 .collect();
878 let metadata: Vec<(String, String)> = metadata
879 .iter()
880 .map(|(k, v)| ((*k).to_string(), (*v).to_string()))
881 .collect();
882 let submitted = nuvec_core::async_eval(&tensors.iter().map(|(_, a)| a).collect::<Vec<_>>());
883 let thread = std::thread::spawn(move || {
884 submitted?;
885 let tensors: Vec<(&str, &Array)> = tensors.iter().map(|(n, a)| (n.as_str(), a)).collect();
886 let metadata: Vec<(&str, &str)> = metadata
887 .iter()
888 .map(|(k, v)| (k.as_str(), v.as_str()))
889 .collect();
890 write_atomically(&path, &encode(&tensors, &metadata)?)
891 });
892 SaveHandle {
893 thread: Some(thread),
894 }
895}
896
897#[cfg(test)]
898mod tests {
899 use super::*;
900
901 #[test]
902 fn read_tensors_start_at_multiples_of_tensor_align() {
903 let json = br#"{"a":{"dtype":"U32","shape":[75],"data_offsets":[0,300]},
907 "b":{"dtype":"BF16","shape":[5],"data_offsets":[300,310]},
908 "skip":{"dtype":"U8","shape":[2],"data_offsets":[310,312]},
909 "c":{"dtype":"U32","shape":[64],"data_offsets":[312,568]},
910 "d":{"dtype":"U32","shape":[1],"data_offsets":[568,572]}}"#;
911 let start = 8 + json.len();
912 let header = parse_header(json, start, start + 572).unwrap();
913 let plan = header.plan(&|name| name != "skip");
914 let offsets: Vec<usize> = plan.tensors.iter().map(|&(_, at)| at).collect();
915 assert_eq!(offsets, [0, 512, 768, 1024]);
916 assert!(offsets.iter().all(|o| o.is_multiple_of(TENSOR_ALIGN)));
917 let s = start as u64;
918 assert_eq!(
919 plan.ranges,
920 [(s, 0, 300), (s + 300, 512, 10), (s + 312, 768, 260)]
921 );
922 assert_eq!(plan.len, 1028);
923 }
924
925 #[test]
926 fn filtered_aligned_loads_map_page_runs_of_the_kept_tensors() {
927 let json = format!(
929 "{:<376}",
930 r#"{"a":{"dtype":"F32","shape":[8],"data_offsets":[0,32]},"#.to_string()
931 + r#""b":{"dtype":"F32","shape":[8],"data_offsets":[32,64]},"#
932 + r#""big":{"dtype":"F32","shape":[64],"data_offsets":[64,320]},"#
933 + r#""e":{"dtype":"F32","shape":[0],"data_offsets":[320,320]},"#
934 + r#""z":{"dtype":"F32","shape":[8],"data_offsets":[320,352]}}"#
935 );
936 assert_eq!(json.len(), 376);
937 let start = 8 + json.len();
938 let len = start + 352;
939 let header = parse_header(json.as_bytes(), start, len).unwrap();
940 assert_eq!(header.runs(len, 64, &|_| true), [(0, 768)]);
942 let some = header.runs(len, 64, &|n| n != "big");
944 assert_eq!(some, [(384, 64), (704, 64)]);
945 assert_eq!(header.runs(len, 64, &|n| n != "z"), [(384, 320)]);
947 assert_eq!(header.runs(len, 64, &|n| n == "e"), []);
949 }
950}