Skip to main content

nuvec_core/
file.rs

1//! Arrays backed by a file's bytes, for loaders (safetensors, npy, ...).
2
3use std::path::Path;
4use std::sync::Arc;
5
6use crate::array::Data;
7use crate::buffer::Buffer;
8use crate::{Array, DType, Error, Result, shape};
9
10/// A file's bytes in array storage: on a Mac with a GPU, the file is memory-mapped and
11/// wrapped as GPU-visible memory without a copy, so arrays made from it are views whose
12/// pages are read when first touched (by the CPU or the GPU). Elsewhere the file is read
13/// into memory. The mapping lives as long as any array made from it.
14///
15/// ```
16/// use nuvec_core::{DType, MappedFile};
17///
18/// # fn main() -> nuvec_core::Result<()> {
19/// # if cfg!(miri) { return Ok(()); } // no file system under Miri's isolation
20/// let path = std::env::temp_dir().join("nuvec-mapped-file-doc.bin");
21/// let bytes: Vec<u8> = [1.0f32, 2.0, 3.0, 4.0].iter().flat_map(|v| v.to_le_bytes()).collect();
22/// std::fs::write(&path, &bytes).unwrap();
23/// let file = MappedFile::open(&path)?;
24/// let a = file.array(4, DType::F32, &[3])?; // bytes 4..16, no copy
25/// assert_eq!(a.to_vec::<f32>()?, vec![2.0, 3.0, 4.0]);
26/// # Ok(())
27/// # }
28/// ```
29#[derive(Clone)]
30pub struct MappedFile {
31    buffer: Arc<Buffer>,
32}
33
34impl std::fmt::Debug for MappedFile {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        f.debug_struct("MappedFile")
37            .field("len", &self.len())
38            .finish()
39    }
40}
41
42impl MappedFile {
43    /// Maps (or reads) the file at `path`.
44    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
45        Ok(Self {
46            buffer: Arc::new(Buffer::map_file(path.as_ref())?),
47        })
48    }
49
50    /// The file's size in bytes.
51    pub fn len(&self) -> usize {
52        self.buffer.len()
53    }
54
55    /// Whether the file is empty.
56    pub fn is_empty(&self) -> bool {
57        self.len() == 0
58    }
59
60    /// The file's bytes.
61    pub fn bytes(&self) -> &[u8] {
62        self.buffer.bytes()
63    }
64
65    /// Bytes `offset..offset + len` as storage of their own: one copy into an aligned buffer,
66    /// for files whose arrays are not aligned to their element sizes (safetensors written
67    /// without header padding, as MLX writes them). Arrays made from the copy are views of
68    /// it, and the file's mapping is not kept.
69    pub fn section(&self, offset: usize, len: usize) -> Result<Self> {
70        if offset.checked_add(len).is_none_or(|end| end > self.len()) {
71            return Err(Error::Io(format!(
72                "{len} bytes at offset {offset} exceed the file's {} bytes",
73                self.len()
74            )));
75        }
76        let mut copy = Buffer::try_alloc(len)?;
77        copy.bytes_mut()
78            .copy_from_slice(&self.bytes()[offset..offset + len]);
79        Ok(Self {
80            buffer: Arc::new(copy),
81        })
82    }
83
84    /// A row-major array of `dtype` and `shape` stored at byte `offset`: a view of the file
85    /// if `offset` is a multiple of the element size, otherwise a copy.
86    pub fn array(&self, offset: usize, dtype: DType, shape: &[usize]) -> Result<Array> {
87        let nbytes = shape::checked_nbytes(shape, dtype.size())?;
88        if offset
89            .checked_add(nbytes)
90            .is_none_or(|end| end > self.len())
91        {
92            return Err(Error::Io(format!(
93                "{nbytes} bytes at offset {offset} exceed the file's {} bytes",
94                self.len()
95            )));
96        }
97        if offset.is_multiple_of(dtype.size()) {
98            let data = Data {
99                buffer: self.buffer.clone(),
100                offset: (offset / dtype.size()) as isize,
101            };
102            return Array::leaf(Ok(data), dtype, shape.into());
103        }
104        let mut copy = Buffer::try_alloc(nbytes)?;
105        copy.bytes_mut()
106            .copy_from_slice(&self.bytes()[offset..offset + nbytes]);
107        Array::from_buffer(copy, dtype, shape)
108    }
109}