Skip to main content

pytorch_reader/
tensor.rs

1//! What the reader produces for each tensor in a checkpoint.
2
3use std::fmt;
4use std::io;
5use std::sync::Arc;
6
7/// Element type of a tensor.
8///
9/// The types `torch.save` writes as a plain storage. Complex, quantized and sparse tensors
10/// are refused at parse time rather than given a variant here. Deliberately exhaustive: a
11/// converter matching on it should fail to compile, not fall through, when a variant is
12/// added, so adding one is a breaking change.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum DType {
15    F64,
16    F32,
17    F16,
18    BF16,
19    I64,
20    I32,
21    I16,
22    I8,
23    U64,
24    U32,
25    U16,
26    U8,
27    /// One byte per element, holding 0 or 1.
28    Bool,
29}
30
31impl DType {
32    /// Size of one element in bytes.
33    pub const fn size(self) -> usize {
34        match self {
35            DType::F64 | DType::I64 | DType::U64 => 8,
36            DType::F32 | DType::I32 | DType::U32 => 4,
37            DType::F16 | DType::BF16 | DType::I16 | DType::U16 => 2,
38            DType::I8 | DType::U8 | DType::Bool => 1,
39        }
40    }
41}
42
43/// A tensor in a checkpoint, with its bytes not yet read.
44///
45/// Opening a checkpoint parses its metadata only. [`read`](Self::read) is what produces the
46/// tensor's bytes, so a checkpoint can be inspected without loading it, or loaded one tensor
47/// at a time. ZIP and legacy containers read the file at that point; a TAR container's
48/// storages are held in memory from open.
49///
50/// The element type and shape are fixed at parse time, when the reader checks that the
51/// tensor fits its storage, so they are read-only: [`byte_len`](Self::byte_len) is what
52/// [`read`](Self::read) returns because nothing can move them apart.
53#[derive(Clone)]
54pub struct Tensor {
55    /// Dotted path of the tensor through the checkpoint's dictionaries
56    /// (`"encoder.0.weight"`). Free to rename; only error messages look at it.
57    pub name: String,
58    dtype: DType,
59    shape: Vec<usize>,
60    read: Arc<dyn Fn() -> io::Result<Vec<u8>> + Send + Sync>,
61}
62
63impl Tensor {
64    pub(crate) fn new(
65        name: String,
66        dtype: DType,
67        shape: Vec<usize>,
68        read: impl Fn() -> io::Result<Vec<u8>> + Send + Sync + 'static,
69    ) -> Self {
70        Self {
71            name,
72            dtype,
73            shape,
74            read: Arc::new(read),
75        }
76    }
77
78    /// Element type.
79    pub fn dtype(&self) -> DType {
80        self.dtype
81    }
82
83    /// Extents, outermost first. Empty for a scalar.
84    pub fn shape(&self) -> &[usize] {
85        &self.shape
86    }
87
88    /// Number of elements.
89    pub fn num_elements(&self) -> usize {
90        self.shape.iter().product()
91    }
92
93    /// Number of bytes [`read`](Self::read) returns.
94    pub fn byte_len(&self) -> usize {
95        self.num_elements() * self.dtype.size()
96    }
97
98    /// Read the tensor's bytes from the checkpoint.
99    ///
100    /// The bytes are contiguous in row-major order and native-endian, whatever layout the
101    /// file stored the tensor in: a strided or offset view is gathered, and a `bool` tensor
102    /// holds only 0 and 1. Nothing is cached, so each call reads the storage again.
103    ///
104    /// Reads from different threads run at once: a ZIP read holds the archive lock only to
105    /// locate its entry. A compressed entry, which `torch.save` does not write, streams
106    /// under the lock instead, as does every entry on a target other than Unix.
107    ///
108    /// # Errors
109    ///
110    /// A storage that is missing, corrupt or unreadable by its container fails with
111    /// [`io::ErrorKind::InvalidData`] (a ZIP storage's checksum is verified when the tensor
112    /// spans all of it, unless the file was saved without checksums, as
113    /// `compute_crc32=False` does), and one shorter than the pickle declared with
114    /// [`io::ErrorKind::UnexpectedEof`]: the file disagrees with itself. Any other kind is
115    /// the operating system's, and means the file could not be read at all. Every error
116    /// names the tensor and its storage.
117    pub fn read(&self) -> io::Result<Vec<u8>> {
118        (self.read)().map_err(|err| named(&self.name, err))
119    }
120
121    /// Split the tensor into its metadata and its read handle.
122    ///
123    /// For a caller that keeps the metadata elsewhere and wants only the handle, without a
124    /// second copy of the name and shape held alive with it. The handle reads and fails
125    /// exactly as [`read`](Self::read) does.
126    pub fn into_parts(
127        self,
128    ) -> (
129        String,
130        DType,
131        Vec<usize>,
132        impl Fn() -> io::Result<Vec<u8>> + Send + Sync + 'static,
133    ) {
134        let Self {
135            name,
136            dtype,
137            shape,
138            read,
139        } = self;
140        let context = name.clone();
141
142        (name, dtype, shape, move || {
143            read().map_err(|err| named(&context, err))
144        })
145    }
146}
147
148impl fmt::Debug for Tensor {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.debug_struct("Tensor")
151            .field("name", &self.name)
152            .field("dtype", &self.dtype)
153            .field("shape", &self.shape)
154            .finish_non_exhaustive()
155    }
156}
157
158/// Put the tensor's name in front of a read error, keeping its kind.
159///
160/// The reader builds a tensor before it knows the name (that is assembled from the dict
161/// path afterwards), so the name is added here rather than inside the handle.
162fn named(name: &str, err: io::Error) -> io::Error {
163    if name.is_empty() {
164        return err;
165    }
166    io::Error::new(err.kind(), format!("tensor '{name}': {err}"))
167}