logicaffeine_compile/model/
safetensors.rs1use std::collections::BTreeMap;
7use std::fs::File;
8use std::io::{self, Read};
9use std::path::{Path, PathBuf};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Dtype {
14 F64,
15 F32,
16 F16,
17 Bf16,
18 F8E4m3,
19 F8E5m2,
20 I64,
21 I32,
22 I16,
23 I8,
24 U8,
25 Bool,
26}
27
28impl Dtype {
29 fn parse(s: &str) -> Option<Dtype> {
30 Some(match s {
31 "F64" => Dtype::F64,
32 "F32" => Dtype::F32,
33 "F16" => Dtype::F16,
34 "BF16" => Dtype::Bf16,
35 "F8_E4M3" => Dtype::F8E4m3,
36 "F8_E5M2" => Dtype::F8E5m2,
37 "I64" => Dtype::I64,
38 "I32" => Dtype::I32,
39 "I16" => Dtype::I16,
40 "I8" => Dtype::I8,
41 "U8" => Dtype::U8,
42 "BOOL" => Dtype::Bool,
43 _ => return None,
44 })
45 }
46
47 pub fn bytes(self) -> usize {
49 match self {
50 Dtype::F64 | Dtype::I64 => 8,
51 Dtype::F32 | Dtype::I32 => 4,
52 Dtype::F16 | Dtype::Bf16 | Dtype::I16 => 2,
53 Dtype::F8E4m3 | Dtype::F8E5m2 | Dtype::I8 | Dtype::U8 | Dtype::Bool => 1,
54 }
55 }
56}
57
58#[derive(Clone, Debug, PartialEq)]
61pub struct Entry {
62 pub dtype: Dtype,
63 pub shape: Vec<u64>,
64 pub begin: u64,
65 pub end: u64,
66}
67
68impl Entry {
69 pub fn elements(&self) -> u64 {
70 self.shape.iter().product()
71 }
72}
73
74pub struct Shard {
76 file: File,
77 path: PathBuf,
78 data_start: u64,
79 pub tensors: BTreeMap<String, Entry>,
80}
81
82fn invalid(msg: impl Into<String>) -> io::Error {
83 io::Error::new(io::ErrorKind::InvalidData, msg.into())
84}
85
86impl Shard {
87 pub fn open(path: &Path) -> io::Result<Shard> {
88 let mut file = File::open(path).map_err(|e| io::Error::new(e.kind(), format!("{}: {e}", path.display())))?;
89 let len = file.metadata()?.len();
90 let mut head = [0u8; 8];
91 file.read_exact(&mut head)?;
92 let header_len = u64::from_le_bytes(head);
93 if header_len > len.saturating_sub(8) {
94 return Err(invalid(format!("{}: a header of {header_len} bytes in a file of {len}", path.display())));
95 }
96 let mut header = vec![0u8; header_len as usize];
97 file.read_exact(&mut header)?;
98 let json: serde_json::Value = serde_json::from_slice(&header)
99 .map_err(|e| invalid(format!("{}: the header is not JSON: {e}", path.display())))?;
100 let object = json.as_object().ok_or_else(|| invalid(format!("{}: the header is not an object", path.display())))?;
101 let data_start = 8 + header_len;
102 let data_len = len - data_start;
103 let mut tensors = BTreeMap::new();
104 for (name, v) in object {
105 if name == "__metadata__" {
106 continue;
107 }
108 let dtype_name = v["dtype"].as_str().unwrap_or_default();
109 let dtype = Dtype::parse(dtype_name)
110 .ok_or_else(|| invalid(format!("{}: {name} has dtype `{dtype_name}`, which is not read", path.display())))?;
111 let shape: Vec<u64> = v["shape"]
112 .as_array()
113 .ok_or_else(|| invalid(format!("{}: {name} has no shape", path.display())))?
114 .iter()
115 .map(|d| d.as_u64().ok_or_else(|| invalid(format!("{}: {name}'s shape is not integers", path.display()))))
116 .collect::<io::Result<_>>()?;
117 let offsets = v["data_offsets"]
118 .as_array()
119 .filter(|a| a.len() == 2)
120 .ok_or_else(|| invalid(format!("{}: {name} has no data offsets", path.display())))?;
121 let (begin, end) = (offsets[0].as_u64().unwrap_or(u64::MAX), offsets[1].as_u64().unwrap_or(u64::MAX));
122 let entry = Entry { dtype, shape, begin, end };
123 if begin > end || end > data_len || end - begin != entry.elements() * dtype.bytes() as u64 {
124 return Err(invalid(format!(
125 "{}: {name} claims bytes {begin}..{end} for {} elements of {dtype_name} in a data section of {data_len}",
126 path.display(),
127 entry.elements()
128 )));
129 }
130 tensors.insert(name.clone(), entry);
131 }
132 Ok(Shard { file, path: path.to_path_buf(), data_start, tensors })
133 }
134
135 pub fn path(&self) -> &Path {
136 &self.path
137 }
138
139 pub fn read(&self, name: &str, offset: u64, len: usize) -> io::Result<Vec<u8>> {
141 let e = self
142 .tensors
143 .get(name)
144 .ok_or_else(|| invalid(format!("{}: no tensor {name}", self.path.display())))?;
145 if offset + len as u64 > e.end - e.begin {
146 return Err(invalid(format!("{}: {name} read past its end", self.path.display())));
147 }
148 let mut out = vec![0u8; len];
149 self.read_at(&mut out, self.data_start + e.begin + offset)?;
150 Ok(out)
151 }
152
153 #[cfg(unix)]
156 fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<()> {
157 use std::os::unix::fs::FileExt;
158 self.file.read_exact_at(buf, offset)
159 }
160
161 #[cfg(windows)]
162 fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<()> {
163 use std::os::windows::fs::FileExt;
164 let mut done = 0;
165 while done < buf.len() {
166 match self.file.seek_read(&mut buf[done..], offset + done as u64)? {
167 0 => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, format!("{}: read past its end", self.path.display()))),
168 n => done += n,
169 }
170 }
171 Ok(())
172 }
173
174 #[cfg(not(any(unix, windows)))]
175 fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<()> {
176 use std::io::{Seek, SeekFrom};
177 let mut file = File::open(&self.path)?;
178 file.seek(SeekFrom::Start(offset))?;
179 file.read_exact(buf)
180 }
181}
182
183pub fn shard_paths(dir: &Path) -> io::Result<Vec<PathBuf>> {
186 let index = dir.join("model.safetensors.index.json");
187 if index.is_file() {
188 let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&index)?)
189 .map_err(|e| invalid(format!("{}: {e}", index.display())))?;
190 let map = json["weight_map"]
191 .as_object()
192 .ok_or_else(|| invalid(format!("{}: no weight_map", index.display())))?;
193 let mut files: Vec<String> = map.values().filter_map(|v| v.as_str().map(str::to_string)).collect();
194 files.sort();
195 files.dedup();
196 let mut out = Vec::new();
197 for f in files {
198 let p = dir.join(&f);
199 if !p.is_file() {
200 return Err(io::Error::new(
201 io::ErrorKind::NotFound,
202 format!("{}: the index names shard {f}, which the directory does not hold", dir.display()),
203 ));
204 }
205 out.push(p);
206 }
207 return Ok(out);
208 }
209 let single = dir.join("model.safetensors");
210 if single.is_file() {
211 return Ok(vec![single]);
212 }
213 let mut out: Vec<PathBuf> = std::fs::read_dir(dir)?
214 .filter_map(|e| e.ok().map(|e| e.path()))
215 .filter(|p| p.extension().is_some_and(|x| x == "safetensors"))
216 .collect();
217 out.sort();
218 if out.is_empty() {
219 return Err(io::Error::new(io::ErrorKind::NotFound, format!("{}: no safetensors file", dir.display())));
220 }
221 Ok(out)
222}