Skip to main content

zyx/
module.rs

1// Copyright (C) 2025 zk4x
2// SPDX-License-Identifier: LGPL-3.0-only WITH Classpath-exception-2.0
3
4use std::{collections::HashMap, ffi::OsStr, fs::File, io::Seek, path::Path};
5
6use crate::{DType, Map, Tensor, ZyxError, shape::Dim};
7
8/// Module trait
9pub trait Module {
10    /// Iterate over all tensors immutably
11    fn iter(&self) -> impl Iterator<Item = &Tensor>;
12
13    /// Iterate over all tensors mutably
14    fn iter_mut(&mut self) -> impl Iterator<Item = &mut Tensor>;
15
16    /// Iterate over tensors without consuming the module
17    fn iter_tensors(&self) -> impl Iterator<Item = (String, &Tensor)>;
18
19    /// From tensors
20    fn iter_tensors_mut(&mut self) -> impl Iterator<Item = (String, &mut Tensor)>;
21
22    /// Set parameters, removes them from params, skips parameters that are not found in params.
23    fn set_params(&mut self, params: &mut HashMap<String, Tensor>) {
24        for (label, tensor) in self.iter_tensors_mut() {
25            if let Some(param) = params.remove(&label) {
26                *tensor = param;
27            }
28        }
29    }
30
31    /// Save tensors or modules to a file determined by file extension.
32    /// Currently only safetensors is supported format.
33    ///
34    /// # Errors
35    ///
36    /// Errors if tensors failed to realize or failed to save to disk.
37    fn save(&self, path: impl AsRef<Path>) -> Result<(), ZyxError> {
38        use std::fmt::Write;
39        use std::io::Write as IOWrite;
40        let mut f = File::create(path)?;
41        let mut header = String::from("{");
42        let mut begin = 0;
43        for (label, tensor) in self.iter_tensors() {
44            let dtype = tensor.dtype();
45            write!(header, "\"{label}\":{{").unwrap();
46            write!(header, "\"dtype\":\"{}\",", dtype.safetensors()).unwrap();
47            let mut st_shape = format!("{:?}", tensor.resolve_shape());
48            st_shape.retain(|c| !c.is_whitespace());
49            write!(header, "\"shape\":{st_shape},").unwrap();
50            let size = tensor.numel().item::<Dim>() * Dim::from(dtype.bit_size() / 8);
51            write!(header, "\"data_offsets\":[{},{}]", begin, begin + size).unwrap();
52            begin += size;
53            write!(header, "}},").unwrap();
54        }
55        header.pop();
56        write!(header, "}}").unwrap();
57        let header_bytes = header.as_bytes();
58        f.write_all(&(header_bytes.len() as i64).to_le_bytes())?;
59        f.write_all(header_bytes)?;
60        for tensor in self.iter() {
61            f.write_all(&tensor.to_le_bytes()?)?;
62        }
63        Ok(())
64    }
65
66    /// Save a single tensor to a `.npy` file (numpy array format).
67    /// Mirrors `load_numpy`: little-endian, C order (Fortran order
68    /// is never written). Header is padded so data starts at a 64-byte
69    /// boundary, like numpy >= 1.9. Numpy files hold a single array, so
70    /// saving a module with more than one tensor is an error.
71    ///
72    /// # Errors
73    ///
74    /// Errors if the module holds more than one tensor, if the tensor
75    /// failed to realize or failed to save to disk.
76    fn save_numpy(&self, path: impl AsRef<Path>) -> Result<(), ZyxError> {
77        use std::io::Write as IOWrite;
78        let mut tensors = self.iter_tensors();
79        let (label, tensor) = match (tensors.next(), tensors.next()) {
80            (Some((label, tensor)), None) => (label, tensor),
81            (None, _) => return Err(ZyxError::parse_error("Cannot save empty module to numpy: no tensors.".into())),
82            (Some((l0, _)), Some((l1, _))) => {
83                return Err(ZyxError::parse_error(
84                    format!(
85                        "Cannot save module to numpy: numpy files hold a single array, module has tensors '{l0}' and '{l1}' (and possibly more)."
86                    )
87                    .into(),
88                ));
89            }
90        };
91        let _ = label;
92        let descr = match tensor.dtype() {
93            DType::F32 => "<f4",
94            DType::F64 => "<f8",
95            DType::F16 => "<f2",
96            DType::I8 => "|i1",
97            DType::I16 => "<i2",
98            DType::I32 => "<i4",
99            DType::I64 => "<i8",
100            DType::U8 => "|u1",
101            DType::U16 => "<u2",
102            DType::BF16 => todo!("BF16 has no numpy dtype"),
103            DType::U32 => todo!("u4 numpy arrays"),
104            DType::U64 => todo!("u8 numpy arrays"),
105            DType::Bool => todo!("Bool numpy arrays"),
106            DType::F8E4M3 => todo!("F8E4M3 numpy arrays"),
107            DType::F8E5M2 => todo!("F8E5M2 numpy arrays"),
108        };
109        let dims = tensor.resolve_shape();
110        let shape_str = format!("({})", dims.iter().map(|d| d.to_string()).collect::<Vec<_>>().join(", "));
111        let mut header = format!("{{'descr': '{descr}', 'fortran_order': False, 'shape': {shape_str}, }}");
112        // magic(6) + version(2) + header_len(2) + header + '\n' must be a
113        // multiple of 64.
114        let total = 6 + 2 + 2 + header.len() + 1;
115        header.extend(core::iter::repeat(' ').take((64 - total % 64) % 64));
116        header.push('\n');
117        let mut f = File::create(path)?;
118        f.write_all(b"\x93NUMPY")?;
119        f.write_all(&[1u8, 0u8])?;
120        f.write_all(&(header.len() as u16).to_le_bytes())?;
121        f.write_all(header.as_bytes())?;
122        f.write_all(&tensor.to_le_bytes()?)?;
123        Ok(())
124    }
125}
126
127/// GGUF metadata value.
128///
129/// Maps one-to-one onto the GGUF file format metadata types
130/// (`TYPE_INT8` .. `TYPE_UINT64`, `TYPE_F32`, `TYPE_F64`, `TYPE_BOOL`,
131/// `TYPE_STRING`), plus `GGUF_ARRAY`, whose elements are recursively
132/// `GGUFMetadataValue`s.
133#[allow(unused)]
134pub enum GGUFMetadataValue {
135    /// Unsigned 8-bit integer
136    Uint8(u8),
137    /// Signed 8-bit integer
138    Int8(i8),
139    /// Unsigned 16-bit integer
140    Uint16(u16),
141    /// Signed 16-bit integer
142    Int16(i16),
143    /// Unsigned 32-bit integer
144    Uint32(u32),
145    /// Signed 32-bit integer
146    Int32(i32),
147    /// Unsigned 64-bit integer
148    Uint64(u64),
149    /// Signed 64-bit integer
150    Int64(i64),
151    /// 32-bit floating-point number
152    Float32(f32),
153    /// 64-bit floating-point number
154    Float64(f64),
155    /// Boolean value
156    Bool(bool),
157    /// UTF-8 string
158    String(String),
159    /// Array of arbitrary metadata values
160    Array(Box<[GGUFMetadataValue]>),
161}
162
163impl<S: std::hash::BuildHasher + Default> Module for HashMap<String, Tensor, S> {
164    fn iter(&self) -> impl Iterator<Item = &Tensor> {
165        self.values()
166    }
167
168    fn iter_mut(&mut self) -> impl Iterator<Item = &mut Tensor> {
169        self.values_mut()
170    }
171
172    fn iter_tensors(&self) -> impl Iterator<Item = (String, &Tensor)> {
173        self.iter().map(|(k, v): (&String, &Tensor)| (k.clone(), v))
174    }
175
176    fn iter_tensors_mut(&mut self) -> impl Iterator<Item = (String, &mut Tensor)> {
177        self.iter_mut().map(|(k, v): (&String, &mut Tensor)| (k.clone(), v))
178    }
179}
180
181impl Module for Vec<Tensor> {
182    #[allow(clippy::into_iter_on_ref)] // into_iter on &Vec/&mut Vec is the existing pattern; changing resolution risks recursion
183    fn iter(&self) -> impl Iterator<Item = &Tensor> {
184        self.into_iter()
185    }
186
187    #[allow(clippy::into_iter_on_ref)] // into_iter on &Vec/&mut Vec is the existing pattern; changing resolution risks recursion
188    fn iter_mut(&mut self) -> impl Iterator<Item = &mut Tensor> {
189        self.into_iter()
190    }
191
192    fn iter_tensors(&self) -> impl Iterator<Item = (String, &Tensor)> {
193        self.iter().map(|t: &Tensor| (format!("{}", t.id()), t))
194    }
195
196    fn iter_tensors_mut(&mut self) -> impl Iterator<Item = (String, &mut Tensor)> {
197        self.iter_mut().map(|t: &mut Tensor| (format!("{}", t.id()), t))
198    }
199}
200
201impl<M0: Module, M1: Module> Module for (M0, M1) {
202    fn iter(&self) -> impl Iterator<Item = &Tensor> {
203        self.0.iter().chain(self.1.iter())
204    }
205
206    fn iter_mut(&mut self) -> impl Iterator<Item = &mut Tensor> {
207        self.0.iter_mut().chain(self.1.iter_mut())
208    }
209
210    fn iter_tensors(&self) -> impl Iterator<Item = (String, &Tensor)> {
211        self.0.iter_tensors().chain(self.1.iter_tensors())
212    }
213
214    fn iter_tensors_mut(&mut self) -> impl Iterator<Item = (String, &mut Tensor)> {
215        self.0.iter_tensors_mut().chain(self.1.iter_tensors_mut())
216    }
217}
218
219impl Tensor {
220    /// Load module from path. This function will determine the filetype based on file extension.
221    ///
222    /// # Errors
223    ///
224    /// Errors if the path has no or an unknown extension, if loading from disk failed
225    /// or if loaded tensors could not be allocated to device.
226    pub fn load(path: impl AsRef<Path>) -> Result<HashMap<String, Tensor>, ZyxError>
227    where
228        Self: Sized,
229    {
230        let e = path.as_ref().extension().and_then(OsStr::to_str);
231        match e {
232            Some("safetensors") => Self::load_safetensors(path),
233            Some("gguf") => Ok(Self::load_gguf(path)?.1),
234            Some(other) => Err(ZyxError::parse_error(
235                format!("Unknown file extension '{other}'. Zyx currently supports only safetensors and gguf formats.").into(),
236            )),
237            None => Err(ZyxError::parse_error(
238                format!("Cannot determine file type: '{}' has no extension. Zyx currently supports only safetensors and gguf formats.", path.as_ref().display()).into(),
239            )),
240        }
241    }
242
243    /// Load gguf module from path
244    /// First returned value is metadata, second returned value are named tensors
245    /// # Errors
246    /// read failure
247    #[allow(clippy::missing_panics_doc)]
248    #[allow(clippy::type_complexity)]
249    pub fn load_gguf(path: impl AsRef<Path>) -> Result<(HashMap<String, GGUFMetadataValue>, HashMap<String, Tensor>), ZyxError> {
250        use std::io::Read;
251        let mut f = std::fs::File::open(&path)?;
252        let mut magic = [0; 4];
253        f.read_exact(&mut magic)?;
254        if magic != *b"GGUF" {
255            if magic == *b"FUGG" {
256                return Err(ZyxError::parse_error(
257                    "GGUF data seems to be stored in big endian order. Only little endian is supported for GGUF in zyx.".into(),
258                ));
259            }
260            return Err(ZyxError::parse_error(format!("Unknown GGUF magic: {magic:?}. Please check your file.").into()));
261        }
262        let mut version_bytes = [0; 4];
263        f.read_exact(&mut version_bytes)?;
264        let version = u32::from_le_bytes(version_bytes);
265        //println!("File size is {} bytes", f.metadata()?.len());
266        let mut tensor_count = [0u8; 8];
267        f.read_exact(&mut tensor_count)?;
268        let tensor_count = u64::from_le_bytes(tensor_count);
269        let mut metadata_kv_count = [0u8; 8];
270        f.read_exact(&mut metadata_kv_count)?;
271        let metadata_kv_count = usize::try_from(u64::from_le_bytes(metadata_kv_count))
272            .map_err(|e| ZyxError::parse_error(format!("Failed to parse tensor count in GGUF file. {e}").into()))?;
273
274        let mut metadata = HashMap::new();
275        for _ in 0..metadata_kv_count {
276            // First string key, (len u64, chars),
277            let mut metadata_key_len = [0; 8];
278            f.read_exact(&mut metadata_key_len)?;
279            let metadata_key_len = u64::from_le_bytes(metadata_key_len);
280            let mut metadata_key_bytes = vec![0u8; usize::try_from(metadata_key_len).unwrap()];
281            f.read_exact(&mut metadata_key_bytes)?;
282            let metadata_key = String::from_utf8(metadata_key_bytes)
283                .map_err(|e| ZyxError::parse_error(format!("GGUF metadata key is not valid UTF-8: {e}").into()))?;
284
285            // Then metadata value type (u32 in GGUF v3, u8 in v1/v2).
286            // Then we the value itself.
287            let metadata_value_type = if version >= 3 {
288                let mut buf = [0; 4];
289                f.read_exact(&mut buf)?;
290                u32::from_le_bytes(buf)
291            } else {
292                let mut buf = [0; 1];
293                f.read_exact(&mut buf)?;
294                u32::from(u8::from_le_bytes(buf))
295            };
296            let metadata_value = match metadata_value_type {
297                0 => {
298                    let mut buf = [0; 1];
299                    f.read_exact(&mut buf)?;
300                    GGUFMetadataValue::Uint8(u8::from_le_bytes(buf))
301                }
302                1 => {
303                    let mut buf = [0; 1];
304                    f.read_exact(&mut buf)?;
305                    GGUFMetadataValue::Int8(i8::from_le_bytes(buf))
306                }
307                2 => {
308                    let mut buf = [0; 2];
309                    f.read_exact(&mut buf)?;
310                    GGUFMetadataValue::Uint16(u16::from_le_bytes(buf))
311                }
312                3 => {
313                    let mut buf = [0; 2];
314                    f.read_exact(&mut buf)?;
315                    GGUFMetadataValue::Int16(i16::from_le_bytes(buf))
316                }
317                4 => {
318                    let mut buf = [0; 4];
319                    f.read_exact(&mut buf)?;
320                    GGUFMetadataValue::Uint32(u32::from_le_bytes(buf))
321                }
322                5 => {
323                    let mut buf = [0; 4];
324                    f.read_exact(&mut buf)?;
325                    GGUFMetadataValue::Int32(i32::from_le_bytes(buf))
326                }
327                6 => {
328                    let mut buf = [0; 4];
329                    f.read_exact(&mut buf)?;
330                    GGUFMetadataValue::Float32(f32::from_le_bytes(buf))
331                }
332                7 => {
333                    let mut buf = [0; 1];
334                    f.read_exact(&mut buf)?;
335                    GGUFMetadataValue::Bool(buf[0] != 0)
336                }
337                8 => {
338                    let mut str_len = [0; 8];
339                    f.read_exact(&mut str_len)?;
340                    let str_len = u64::from_le_bytes(str_len);
341                    let mut s_bytes = vec![0u8; usize::try_from(str_len).unwrap()];
342                    f.read_exact(&mut s_bytes)?;
343                    let s = String::from_utf8(s_bytes)
344                        .map_err(|e| ZyxError::parse_error(format!("GGUF metadata string is not valid UTF-8: {e}").into()))?;
345                    GGUFMetadataValue::String(s)
346                }
347                9 => {
348                    let mut arr_type_buf = [0; 4];
349                    f.read_exact(&mut arr_type_buf)?;
350                    let elem_type = u32::from_le_bytes(arr_type_buf);
351                    let mut arr_len_buf = [0; 8];
352                    f.read_exact(&mut arr_len_buf)?;
353                    let arr_len = u64::from_le_bytes(arr_len_buf);
354                    let mut items = Vec::with_capacity(usize::try_from(arr_len).unwrap());
355                    for _ in 0..arr_len {
356                        let item = match elem_type {
357                            0 => {
358                                let mut buf = [0; 1];
359                                f.read_exact(&mut buf)?;
360                                GGUFMetadataValue::Uint8(u8::from_le_bytes(buf))
361                            }
362                            1 => {
363                                let mut buf = [0; 1];
364                                f.read_exact(&mut buf)?;
365                                GGUFMetadataValue::Int8(i8::from_le_bytes(buf))
366                            }
367                            2 => {
368                                let mut buf = [0; 2];
369                                f.read_exact(&mut buf)?;
370                                GGUFMetadataValue::Uint16(u16::from_le_bytes(buf))
371                            }
372                            3 => {
373                                let mut buf = [0; 2];
374                                f.read_exact(&mut buf)?;
375                                GGUFMetadataValue::Int16(i16::from_le_bytes(buf))
376                            }
377                            4 => {
378                                let mut buf = [0; 4];
379                                f.read_exact(&mut buf)?;
380                                GGUFMetadataValue::Uint32(u32::from_le_bytes(buf))
381                            }
382                            5 => {
383                                let mut buf = [0; 4];
384                                f.read_exact(&mut buf)?;
385                                GGUFMetadataValue::Int32(i32::from_le_bytes(buf))
386                            }
387                            6 => {
388                                let mut buf = [0; 4];
389                                f.read_exact(&mut buf)?;
390                                GGUFMetadataValue::Float32(f32::from_le_bytes(buf))
391                            }
392                            7 => {
393                                let mut buf = [0; 1];
394                                f.read_exact(&mut buf)?;
395                                GGUFMetadataValue::Bool(buf[0] != 0)
396                            }
397                            8 => {
398                                let mut item_len = [0; 8];
399                                f.read_exact(&mut item_len)?;
400                                let item_len = u64::from_le_bytes(item_len);
401                                let mut item_bytes = vec![0u8; usize::try_from(item_len).unwrap()];
402                                f.read_exact(&mut item_bytes)?;
403                                let item = String::from_utf8(item_bytes).map_err(|e| {
404                                    ZyxError::parse_error(format!("GGUF array element string is not valid UTF-8: {e}").into())
405                                })?;
406                                GGUFMetadataValue::String(item)
407                            }
408                            10 => {
409                                let mut buf = [0; 8];
410                                f.read_exact(&mut buf)?;
411                                GGUFMetadataValue::Uint64(u64::from_le_bytes(buf))
412                            }
413                            11 => {
414                                let mut buf = [0; 8];
415                                f.read_exact(&mut buf)?;
416                                GGUFMetadataValue::Int64(i64::from_le_bytes(buf))
417                            }
418                            12 => {
419                                let mut buf = [0; 8];
420                                f.read_exact(&mut buf)?;
421                                GGUFMetadataValue::Float64(f64::from_le_bytes(buf))
422                            }
423                            x => todo!("GGUF array element type {x} not supported"),
424                        };
425                        items.push(item);
426                    }
427                    GGUFMetadataValue::Array(items.into_boxed_slice())
428                }
429                10 => {
430                    let mut buf = [0; 8];
431                    f.read_exact(&mut buf)?;
432                    GGUFMetadataValue::Uint64(u64::from_le_bytes(buf))
433                }
434                11 => {
435                    let mut buf = [0; 8];
436                    f.read_exact(&mut buf)?;
437                    GGUFMetadataValue::Int64(i64::from_le_bytes(buf))
438                }
439                12 => {
440                    let mut buf = [0; 8];
441                    f.read_exact(&mut buf)?;
442                    GGUFMetadataValue::Float64(f64::from_le_bytes(buf))
443                }
444                x => todo!("GGUF metadata type {x} not supported"),
445            };
446            metadata.insert(metadata_key, metadata_value);
447        }
448
449        // First we read the whole description of tensors
450        let mut tensor_header = Map::default();
451        for _ in 0..tensor_count {
452            // name
453            let mut tensor_name_len = [0; 8];
454            f.read_exact(&mut tensor_name_len)?;
455            let tensor_name_len = u64::from_le_bytes(tensor_name_len);
456            let mut tensor_name_bytes = vec![0u8; usize::try_from(tensor_name_len).unwrap()];
457            f.read_exact(&mut tensor_name_bytes)?;
458            let tensor_name = String::from_utf8(tensor_name_bytes)
459                .map_err(|e| ZyxError::parse_error(format!("GGUF tensor name is not valid UTF-8: {e}").into()))?;
460
461            // rank (number of dimensions)
462            let mut rank = [0; 4];
463            f.read_exact(&mut rank)?;
464            let rank = u32::from_le_bytes(rank);
465
466            // shape (NOTE there is no explicit check for endiannes here)
467            let mut shape = vec![0u8; rank as usize * 8];
468            f.read_exact(&mut shape)?;
469            let shape: Vec<Dim> =
470                shape.chunks_exact(8).map(|x| i64::from_le_bytes([x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7]])).collect();
471
472            // dtype
473            let mut dtype = [0; 4];
474            f.read_exact(&mut dtype)?;
475            let dtype = u32::from_le_bytes(dtype);
476            // Q4_K (gguf type 12) loads as raw super-blocks: [num_blocks, 144]
477            // U8. Each 144B block holds 256 weights (d, dmin, 12 scale bytes,
478            // 128B of nibbles, llama.cpp `block_q4_K`). Element dims are not
479            // representable at sub-byte granularity, so the caller derives
480            // (rows, cols) from the model hyperparams + tensor name.
481            let (dtype, shape) = match dtype {
482                0 => (DType::F32, shape),
483                1 => (DType::F16, shape),
484                24 => (DType::I8, shape),
485                25 => (DType::I16, shape),
486                26 => (DType::I32, shape),
487                27 => (DType::I64, shape),
488                28 => (DType::F64, shape),
489                12 => {
490                    let numel: Dim = shape.iter().product();
491                    debug_assert!(numel % 256 == 0, "Q4_K tensor {tensor_name} has {numel} elements, not a multiple of 256");
492                    (DType::U8, vec![numel / 256, 144])
493                }
494                // Q8_0: block_q8_0, 34B per 32 (static_assert: half + QK8_0).
495                8 => {
496                    let numel: Dim = shape.iter().product();
497                    debug_assert!(numel % 32 == 0, "Q8_0 tensor {tensor_name} has {numel} elements, not a multiple of 32");
498                    (DType::U8, vec![numel / 32, 34])
499                }
500                // Q3_K: block_q3_K, 110B per 256 (half + 64 + 32 + 12).
501                11 => {
502                    let numel: Dim = shape.iter().product();
503                    debug_assert!(numel % 256 == 0, "Q3_K tensor {tensor_name} has {numel} elements, not a multiple of 256");
504                    (DType::U8, vec![numel / 256, 110])
505                }
506                // Q5_K: block_q5_K, 176B per 256 (2*half + 12 + 128 + 32).
507                13 => {
508                    let numel: Dim = shape.iter().product();
509                    debug_assert!(numel % 256 == 0, "Q5_K tensor {tensor_name} has {numel} elements, not a multiple of 256");
510                    (DType::U8, vec![numel / 256, 176])
511                }
512                // Q6_K: block_q6_K, 210B per 256 (half + 16 + 192).
513                14 => {
514                    let numel: Dim = shape.iter().product();
515                    debug_assert!(numel % 256 == 0, "Q6_K tensor {tensor_name} has {numel} elements, not a multiple of 256");
516                    (DType::U8, vec![numel / 256, 210])
517                }
518                // IQ4_NL: block_iq4_nl, 18B per 32 (half + QK4_NL/2).
519                20 => {
520                    let numel: Dim = shape.iter().product();
521                    debug_assert!(numel % 32 == 0, "IQ4_NL tensor {tensor_name} has {numel} elements, not a multiple of 32");
522                    (DType::U8, vec![numel / 32, 18])
523                }
524                // IQ3_S: block_iq3_s, 110B per 256 (half + 104 + 4).
525                21 => {
526                    let numel: Dim = shape.iter().product();
527                    debug_assert!(numel % 256 == 0, "IQ3_S tensor {tensor_name} has {numel} elements, not a multiple of 256");
528                    (DType::U8, vec![numel / 256, 110])
529                }
530                // IQ4_XS: block_iq4_xs, 136B per 256 (half + u16 + 4 + 128).
531                23 => {
532                    let numel: Dim = shape.iter().product();
533                    debug_assert!(numel % 256 == 0, "IQ4_XS tensor {tensor_name} has {numel} elements, not a multiple of 256");
534                    (DType::U8, vec![numel / 256, 136])
535                }
536                // Q4_0 (gguf type 2) loads as raw super-blocks: [num_blocks, 18]
537                // U8. Each 18B block holds 32 weights (half d + 16B of nibbles,
538                // llama.cpp `block_q4_0`).
539                2 => {
540                    let numel: Dim = shape.iter().product();
541                    debug_assert!(numel % 32 == 0, "Q4_0 tensor {tensor_name} has {numel} elements, not a multiple of 32");
542                    (DType::U8, vec![numel / 32, 18])
543                }
544                // Q4_1 (gguf type 3): block_q4_1, 20B per 32 (2*half + 16B nibbles).
545                3 => {
546                    let numel: Dim = shape.iter().product();
547                    debug_assert!(numel % 32 == 0, "Q4_1 tensor {tensor_name} has {numel} elements, not a multiple of 32");
548                    (DType::U8, vec![numel / 32, 20])
549                }
550                // Q5_0 (gguf type 6): block_q5_0, 22B per 32 (half + 4B qh + 16B qs).
551                6 => {
552                    let numel: Dim = shape.iter().product();
553                    debug_assert!(numel % 32 == 0, "Q5_0 tensor {tensor_name} has {numel} elements, not a multiple of 32");
554                    (DType::U8, vec![numel / 32, 22])
555                }
556                // Q5_1 (gguf type 7): block_q5_1, 24B per 32 (2*half + 4B qh + 16B qs).
557                7 => {
558                    let numel: Dim = shape.iter().product();
559                    debug_assert!(numel % 32 == 0, "Q5_1 tensor {tensor_name} has {numel} elements, not a multiple of 32");
560                    (DType::U8, vec![numel / 32, 24])
561                }
562                // Q8_1 (gguf type 9): block_q8_1, 36B per 32 (2*half + 32B qs).
563                9 => {
564                    let numel: Dim = shape.iter().product();
565                    debug_assert!(numel % 32 == 0, "Q8_1 tensor {tensor_name} has {numel} elements, not a multiple of 32");
566                    (DType::U8, vec![numel / 32, 36])
567                }
568                // Q2_K (gguf type 10): block_q2_K, 84B per 256 (2*half + 16B scales + 64B qs).
569                10 => {
570                    let numel: Dim = shape.iter().product();
571                    debug_assert!(numel % 256 == 0, "Q2_K tensor {tensor_name} has {numel} elements, not a multiple of 256");
572                    (DType::U8, vec![numel / 256, 84])
573                }
574                // Q8_K (gguf type 15): block_q8_K, 292B per 256 (float + 256B qs + 16 i16 sums).
575                15 => {
576                    let numel: Dim = shape.iter().product();
577                    debug_assert!(numel % 256 == 0, "Q8_K tensor {tensor_name} has {numel} elements, not a multiple of 256");
578                    (DType::U8, vec![numel / 256, 292])
579                }
580                // IQ2_XXS (gguf type 16): block_iq2_xxs, 66B per 256 (half + 32 u16 qs).
581                16 => {
582                    let numel: Dim = shape.iter().product();
583                    debug_assert!(numel % 256 == 0, "IQ2_XXS tensor {tensor_name} has {numel} elements, not a multiple of 256");
584                    (DType::U8, vec![numel / 256, 66])
585                }
586                // IQ2_XS (gguf type 17): block_iq2_xs, 74B per 256 (half + 64B qs + 8B scales).
587                17 => {
588                    let numel: Dim = shape.iter().product();
589                    debug_assert!(numel % 256 == 0, "IQ2_XS tensor {tensor_name} has {numel} elements, not a multiple of 256");
590                    (DType::U8, vec![numel / 256, 74])
591                }
592                // IQ3_XXS (gguf type 18): block_iq3_xxs, 98B per 256 (half + 96B qs).
593                18 => {
594                    let numel: Dim = shape.iter().product();
595                    debug_assert!(numel % 256 == 0, "IQ3_XXS tensor {tensor_name} has {numel} elements, not a multiple of 256");
596                    (DType::U8, vec![numel / 256, 98])
597                }
598                // IQ1_S (gguf type 19): block_iq1_s, 50B per 256 (half + 32B qs + 16B qh).
599                19 => {
600                    let numel: Dim = shape.iter().product();
601                    debug_assert!(numel % 256 == 0, "IQ1_S tensor {tensor_name} has {numel} elements, not a multiple of 256");
602                    (DType::U8, vec![numel / 256, 50])
603                }
604                // IQ2_S (gguf type 22): block_iq2_s, 82B per 256 (half + 64B qs + 8B qh + 8B scales).
605                22 => {
606                    let numel: Dim = shape.iter().product();
607                    debug_assert!(numel % 256 == 0, "IQ2_S tensor {tensor_name} has {numel} elements, not a multiple of 256");
608                    (DType::U8, vec![numel / 256, 82])
609                }
610                // IQ1_M (gguf type 29): block_iq1_m, 56B per 256 (32B qs + 16B qh + 8B scales, no fp scale).
611                29 => {
612                    let numel: Dim = shape.iter().product();
613                    debug_assert!(numel % 256 == 0, "IQ1_M tensor {tensor_name} has {numel} elements, not a multiple of 256");
614                    (DType::U8, vec![numel / 256, 56])
615                }
616                x => todo!("GGUF dtype {x} is not supported by zyx yet."),
617            };
618
619            // offset (position in file)
620            let mut offset = [0; 8];
621            f.read_exact(&mut offset)?;
622            let offset = u64::from_le_bytes(offset);
623
624            tensor_header.insert(tensor_name, (shape, dtype, offset));
625        }
626
627        // GGUF tensor offsets are relative to the data section, which starts
628        // right after the tensor infos, aligned up to `general.alignment`
629        // (spec default 32). The offsets must not be used as raw file
630        // offsets.
631        let alignment = match metadata.get("general.alignment") {
632            Some(GGUFMetadataValue::Uint32(a)) => (*a as usize).max(1),
633            Some(_) => todo!("general.alignment must be Uint32"),
634            None => 32,
635        };
636        let data_start = f.stream_position()? as usize;
637        let data_start = data_start.div_ceil(alignment) * alignment;
638
639        let mut progress_bar = if crate::debug_mask().dev() {
640            println!("Loading tensors from safetensors file");
641            let bar = crate::progress::ProgressBar::new(tensor_count);
642            Some(bar)
643        } else {
644            None
645        };
646
647        let mut tensors = HashMap::new();
648        for (name, (shape, dtype, offset)) in tensor_header {
649            if let Some(progress_bar) = &mut progress_bar {
650                progress_bar.inc(1, &format!("{name}, {shape:?}, {dtype}"));
651            }
652            tensors.insert(name, Tensor::from_path(shape, dtype, &path, (data_start as u64) + offset)?);
653        }
654        Ok((metadata, tensors))
655    }
656
657    /// Load a single `.npy` array from path.
658    ///
659    /// Reads the array lazily from disk (no host copy until realize), like
660    /// [`Self::load_gguf`] and [`Self::load_safetensors`]. Supports little
661    /// -endian numeric dtypes; big-endian files, Fortran order and non
662    /// -numeric dtypes are loud errors, never guesses.
663    ///
664    /// # Errors
665    /// Errors if the path does not exist, IO failed, or the file uses an
666    /// unsupported dtype, byte order or memory order.
667    pub fn load_numpy(path: impl AsRef<Path>) -> Result<Tensor, ZyxError> {
668        use std::io::Read;
669        let path = path.as_ref();
670        let mut f = File::open(path)?;
671        let mut magic = [0; 6];
672        f.read_exact(&mut magic)?;
673        if magic != *b"\x93NUMPY" {
674            return Err(ZyxError::parse_error(format!("Unknown numpy magic: {magic:?} in {path:?}").into()));
675        }
676        let mut ver = [0; 2];
677        f.read_exact(&mut ver)?;
678        // v1.0 header len is u16, v2.0+ is u32.
679        let header_len = match ver[0] {
680            1 => {
681                let mut buf = [0; 2];
682                f.read_exact(&mut buf)?;
683                u16::from_le_bytes(buf) as usize
684            }
685            2 | 3 => {
686                let mut buf = [0; 4];
687                f.read_exact(&mut buf)?;
688                u32::from_le_bytes(buf) as usize
689            }
690            x => return Err(ZyxError::parse_error(format!("Unsupported numpy version {x} in {path:?}").into())),
691        };
692        let mut header = vec![0u8; header_len];
693        f.read_exact(&mut header)?;
694        let header = String::from_utf8(header)
695            .map_err(|e| ZyxError::parse_error(format!("numpy header is not valid UTF-8: {e} in {path:?}").into()))?;
696        // Header is a python dict literal: {'descr': '<f4', 'fortran_order': False, 'shape': (2, 3), }
697        let field = |key: &str| -> Option<String> {
698            let start = header.find(&format!("'{key}':"))? + key.len() + 4;
699            // Value ends at the next top-level ',' or '}'.
700            let rest = &header[start..];
701            let end = rest.find(|c| c == ',' || c == '}').unwrap_or(rest.len());
702            Some(rest[..end].trim().to_string())
703        };
704        let descr =
705            field("descr").ok_or_else(|| ZyxError::parse_error(format!("numpy header missing 'descr' in {path:?}").into()))?;
706        let descr = descr.trim_matches(|c| c == '\'' || c == '"').to_string();
707        let fortran = field("fortran_order").unwrap_or_default();
708        if fortran.contains("True") {
709            return Err(ZyxError::parse_error(format!("Fortran-order numpy arrays are not supported: {path:?}").into()));
710        }
711        // The shape value is a tuple "(2, 3)" containing commas itself, so
712        // it cannot go through `field`, which stops at the first ','. Take
713        // everything up to the closing '}' of the dict instead.
714        let shape_start = header
715            .find("'shape':")
716            .ok_or_else(|| ZyxError::parse_error(format!("numpy header missing 'shape' in {path:?}").into()))?
717            + 8;
718        let rest = &header[shape_start..];
719        let end = rest.find('}').unwrap_or(rest.len());
720        let shape_str = rest[..end].trim().trim_end_matches(',').trim();
721        let shape_str = shape_str.trim_matches(|c| c == '(' || c == ')');
722        let shape: Vec<Dim> = shape_str
723            .split(',')
724            .filter(|d| !d.trim().is_empty())
725            .map(|d| {
726                d.trim()
727                    .parse::<Dim>()
728                    .map_err(|e| ZyxError::parse_error(format!("Cannot parse numpy shape '{shape_str}': {e} in {path:?}").into()))
729            })
730            .collect::<Result<_, ZyxError>>()?;
731        let dtype = match descr.as_str() {
732            "<f4" | "|f4" | "f4" => DType::F32,
733            "<f2" | "|f2" | "f2" => DType::F16,
734            "<f8" | "|f8" | "f8" => DType::F64,
735            "<i1" | "|i1" => DType::I8,
736            "<i2" | "|i2" => DType::I16,
737            "<i4" | "|i4" => DType::I32,
738            "<i8" | "|i8" => DType::I64,
739            "|u1" | "<u1" | "u1" => DType::U8,
740            "<u2" | "|u2" => DType::U16,
741            "<u4" | "|u4" => todo!("u4 numpy arrays"),
742            "<u8" | "|u8" => todo!("u8 numpy arrays"),
743            x => todo!("numpy dtype '{x}' is not supported ({path:?})"),
744        };
745        // numpy >= 1.9 pads the header so data starts at a 64-byte boundary;
746        // the padding is already counted in header_len, so the stream
747        // position after the header is the data start.
748        let data_start = f.stream_position()?;
749        Tensor::from_path(shape, dtype, path, data_start)
750    }
751
752    /// Load safetensors module from path
753    ///
754    /// # Errors
755    /// Errors if path does not exist or IO failed for other reasons.
756    #[allow(clippy::missing_panics_doc)]
757    pub fn load_safetensors(path: impl AsRef<Path>) -> Result<HashMap<String, Tensor>, ZyxError> {
758        use std::io::Read;
759        let mut f = std::fs::File::open(&path)?;
760        //println!("File size is {} bytes", f.metadata()?.len());
761        let mut header_len = [0u8; 8];
762        f.read_exact(&mut header_len)?;
763        let n = usize::try_from(u64::from_le_bytes(header_len))
764            .map_err(|e| ZyxError::parse_error(format!("Failed to parse header len in safetensors file. {e}").into()))?;
765        let mut header = vec![0u8; n];
766        f.read_exact(&mut header)?;
767        let header = core::str::from_utf8(&header).map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
768        let mut text = String::with_capacity(10);
769        let mut begin_str = false;
770        let mut i = 0;
771        let mut tensors = HashMap::default();
772        let mut dtype = DType::F32;
773        let mut shape = vec![1i64];
774        let mut label = String::new();
775        let mut metadata = true;
776        let mut progress_bar = if crate::debug_mask().dev() {
777            println!("Loading tensors from safetensors file");
778            let bar = crate::progress::ProgressBar::new(u64::try_from(header.chars().filter(|&c| c == '[').count()).unwrap() / 2);
779            Some(bar)
780        } else {
781            None
782        };
783        //let mmap = Arc::new(unsafe { memmap2::Mmap::map(&f)? });
784        //let mut mptr = mmap.as_ptr();
785        //mptr = mptr.wrapping_add(8 + header.len());
786        let mut offset = (8 + header.len()) as i64;
787        for x in header.chars() {
788            // We skip metadata for now
789            if metadata && text.starts_with("__metadata__") {
790                if x == '}' {
791                    text.clear();
792                    begin_str = false;
793                    metadata = false;
794                }
795                continue;
796            }
797            if ['"', '[', ']'].contains(&x) {
798                if begin_str {
799                    //std::println!("{text}");
800                    if i % 7 == 0 {
801                        #[allow(clippy::assigning_clones)]
802                        {
803                            label = text.clone();
804                        }
805                    } else if i % 7 == 2 {
806                        dtype = DType::from_safetensors(&text)?;
807                    } else if i % 7 == 4 {
808                        shape = text
809                            .split(',')
810                            .map(|d| {
811                                d.parse::<Dim>()
812                                    .map_err(|err| ZyxError::parse_error(format!("Cannot parse safetensors shape: {err}").into()))
813                            })
814                            .collect::<Result<_, ZyxError>>()?;
815                    } else if i % 7 == 6 {
816                        // TODO assert offsets
817                        //println!("Offsets: {text}");
818                        let offsets = text
819                            .split(',')
820                            .map(|offset| {
821                                // Whitespace after commas is valid JSON; the
822                                // scanner keeps it in `text`, so trim first.
823                                offset.trim().parse::<u64>().map_err(|err| {
824                                    ZyxError::parse_error(format!("Could not parse safetensors offset: {err}").into())
825                                })
826                            })
827                            .collect::<Result<Vec<_>, ZyxError>>()?;
828                        //println!("Offsets: {offsets:?}");
829                        let bytes = shape.iter().product::<Dim>() * Dim::from(dtype.bit_size() / 8);
830                        if offsets[1] - offsets[0] != bytes as u64 {
831                            return Err(ZyxError::parse_error("Safetensors shapes and offsets are incorrect.".into()));
832                        }
833                        if let Some(bar) = &mut progress_bar {
834                            bar.inc(1, &format!("{label}, {shape:?}, {dtype:?}"));
835                        }
836                        let tensor = Tensor::from_path(shape.clone(), dtype, &path, offset as u64)?;
837                        offset += bytes as i64;
838                        tensors.insert(label.clone(), tensor);
839                    }
840                    i += 1;
841                    text.clear();
842                    begin_str = false;
843                } else {
844                    text.clear();
845                    begin_str = true;
846                }
847            } else {
848                text.push(x);
849            }
850        }
851        Ok(tensors)
852    }
853}