1use anyhow::{bail, Context, Result};
13use serde::Serialize;
14use std::collections::BTreeMap;
15use std::io::Read;
16use std::path::Path;
17
18use crate::ir::Key;
19use crate::model_ir::{Confidence, ModelIr, Parameter, ParameterRole};
20
21#[derive(Debug, Clone)]
22pub struct TensorInfo {
23 pub shape: Vec<usize>,
24 pub dtype: String,
25}
26
27pub type Header = BTreeMap<String, TensorInfo>;
28const MAX_HEADER_BYTES: u64 = 100_000_000;
29
30pub fn read_header(path: &Path) -> Result<Header> {
35 let mut file =
36 std::fs::File::open(path).with_context(|| format!("opening {}", path.display()))?;
37 let file_len = file
38 .metadata()
39 .with_context(|| format!("reading metadata for {}", path.display()))?
40 .len();
41
42 let mut prefix = [0u8; 8];
43 file.read_exact(&mut prefix)
44 .with_context(|| format!("{} is too short to be a safetensors file", path.display()))?;
45 let header_len_u64 = u64::from_le_bytes(prefix);
46 if header_len_u64 > MAX_HEADER_BYTES {
47 bail!(
48 "{} declares a {} byte header, exceeding the {} byte safety limit",
49 path.display(),
50 header_len_u64,
51 MAX_HEADER_BYTES
52 );
53 }
54 let header_end = 8u64
55 .checked_add(header_len_u64)
56 .filter(|end| *end <= file_len)
57 .with_context(|| format!("{} declares a header longer than the file", path.display()))?;
58 let header_len = usize::try_from(header_len_u64)
59 .with_context(|| format!("safetensors header of {} is too large", path.display()))?;
60
61 let mut bytes = vec![0u8; header_len];
62 file.read_exact(&mut bytes)
63 .with_context(|| format!("reading safetensors header of {}", path.display()))?;
64
65 debug_assert_eq!(header_end, 8 + header_len as u64);
66 let json: serde_json::Value = serde_json::from_slice(&bytes)
67 .with_context(|| format!("parsing safetensors header of {}", path.display()))?;
68 let obj = json
69 .as_object()
70 .with_context(|| format!("safetensors header of {} is not an object", path.display()))?;
71
72 let data_len = file_len - header_end;
73 let mut header = Header::new();
74 let mut occupied: Vec<(u64, u64, String)> = Vec::new();
75
76 for (name, value) in obj {
77 if name == "__metadata__" {
78 continue;
79 }
80 let info = parse_tensor_entry(name, value, data_len)
81 .with_context(|| format!("tensor `{name}` in {}", path.display()))?;
82 occupied.push((info.offset_start, info.offset_end, name.clone()));
83 header.insert(
84 name.clone(),
85 TensorInfo {
86 shape: info.shape,
87 dtype: info.dtype,
88 },
89 );
90 }
91
92 occupied.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
93 for window in occupied.windows(2) {
94 let (a_start, a_end, a_name) = &window[0];
95 let (b_start, _b_end, b_name) = &window[1];
96 if *b_start < *a_end {
97 bail!(
98 "overlapping data_offsets in {}: `{}` [{a_start},{a_end}) overlaps `{b_name}` starting at {b_start}",
99 path.display(),
100 a_name
101 );
102 }
103 }
104
105 Ok(header)
106}
107
108struct ParsedTensor {
109 shape: Vec<usize>,
110 dtype: String,
111 offset_start: u64,
112 offset_end: u64,
113}
114
115fn parse_tensor_entry(
116 name: &str,
117 value: &serde_json::Value,
118 data_len: u64,
119) -> Result<ParsedTensor> {
120 let obj = value
121 .as_object()
122 .with_context(|| format!("entry `{name}` is not an object"))?;
123
124 let shape_value = obj
125 .get("shape")
126 .with_context(|| format!("tensor `{name}` is missing shape"))?;
127 let shape_arr = shape_value
128 .as_array()
129 .with_context(|| format!("tensor `{name}` shape must be an array"))?;
130 let mut shape = Vec::with_capacity(shape_arr.len());
131 for (i, dim) in shape_arr.iter().enumerate() {
132 let n = dim.as_u64().with_context(|| {
133 format!("tensor `{name}` shape[{i}] must be a non-negative integer")
134 })?;
135 let n = usize::try_from(n)
136 .with_context(|| format!("tensor `{name}` shape[{i}] = {n} is too large"))?;
137 shape.push(n);
138 }
139
140 let dtype = obj
141 .get("dtype")
142 .and_then(|d| d.as_str())
143 .with_context(|| format!("tensor `{name}` is missing a string dtype"))?
144 .to_string();
145 let element_size = dtype_nbytes(&dtype)
146 .with_context(|| format!("tensor `{name}` has unsupported or unknown dtype `{dtype}`"))?;
147
148 let offsets_value = obj
149 .get("data_offsets")
150 .with_context(|| format!("tensor `{name}` is missing data_offsets"))?;
151 let offsets = offsets_value
152 .as_array()
153 .with_context(|| format!("tensor `{name}` data_offsets must be an array"))?;
154 if offsets.len() != 2 {
155 bail!("tensor `{name}` data_offsets must have exactly two entries");
156 }
157 let offset_start = offsets[0].as_u64().with_context(|| {
158 format!("tensor `{name}` data_offsets[0] must be a non-negative integer")
159 })?;
160 let offset_end = offsets[1].as_u64().with_context(|| {
161 format!("tensor `{name}` data_offsets[1] must be a non-negative integer")
162 })?;
163 if offset_end < offset_start {
164 bail!("tensor `{name}` data_offsets end {offset_end} is before start {offset_start}");
165 }
166 if offset_end > data_len {
167 bail!(
168 "tensor `{name}` data_offsets end {offset_end} exceeds data region length {data_len}"
169 );
170 }
171
172 let declared = offset_end - offset_start;
173 let expected = tensor_nbytes(&shape, element_size)
174 .with_context(|| format!("tensor `{name}` byte size overflows"))?;
175 if declared != expected {
176 bail!(
177 "tensor `{name}` data_offsets span {declared} bytes but shape {:?} dtype {dtype} requires {expected}",
178 shape
179 );
180 }
181
182 Ok(ParsedTensor {
183 shape,
184 dtype,
185 offset_start,
186 offset_end,
187 })
188}
189
190fn dtype_nbytes(dtype: &str) -> Result<u64> {
191 let n = match dtype {
192 "BOOL" | "U8" | "I8" | "F8_E4M3" | "F8_E5M2" => 1u64,
193 "I16" | "U16" | "F16" | "BF16" => 2,
194 "I32" | "U32" | "F32" => 4,
195 "I64" | "U64" | "F64" => 8,
196 _ => bail!("unsupported safetensors dtype `{dtype}`"),
197 };
198 Ok(n)
199}
200
201fn tensor_nbytes(shape: &[usize], element_size: u64) -> Result<u64> {
202 let mut n = element_size;
203 for &dim in shape {
204 let dim = u64::try_from(dim).context("shape dimension does not fit u64")?;
205 n = n
206 .checked_mul(dim)
207 .context("shape × dtype byte size overflows u64")?;
208 }
209 Ok(n)
210}
211
212#[derive(Debug, Default, Serialize)]
213pub struct VerifyReport {
214 pub root: String,
216 pub matched: usize,
218 pub missing_certain: Vec<String>,
221 pub missing_conditional: Vec<String>,
224 pub unclaimed: Vec<String>,
226 pub skipped_other_root: usize,
228 pub checkpoint_tensors: usize,
229}
230
231pub fn verify_model(model: &mut ModelIr, header: &Header, root: &str) -> VerifyReport {
236 let mut report = VerifyReport {
237 checkpoint_tensors: header.len(),
238 root: root.to_string(),
239 ..Default::default()
240 };
241 let mut claimed: Vec<bool> = vec![false; header.len()];
242 let names: Vec<&String> = header.keys().collect();
243
244 for parameter in &mut model.parameters {
245 if parameter.builder_root != root {
246 report.skipped_other_root += 1;
247 continue;
248 }
249 let key = Key::from_dotted(¶meter.key);
250
251 if key.is_template() {
252 let hits: Vec<usize> = names
253 .iter()
254 .enumerate()
255 .filter(|(_, name)| key.matches(name))
256 .map(|(i, _)| i)
257 .collect();
258 if hits.is_empty() {
259 record_missing(&mut report, parameter, parameter.key.clone());
260 parameter.checkpoint_shape = None;
261 parameter.checkpoint_dtype = None;
262 } else {
263 for hit in &hits {
264 claimed[*hit] = true;
265 }
266 let info = &header[names[hits[0]]];
267 parameter.checkpoint_shape = Some(info.shape.clone());
268 parameter.checkpoint_dtype = Some(info.dtype.clone());
269 report.matched += 1;
270 }
271 continue;
272 }
273
274 let exact = parameter.key.clone();
275 match names.iter().position(|n| **n == exact) {
276 Some(hit) => {
277 claimed[hit] = true;
278 let info = &header[names[hit]];
279 parameter.checkpoint_shape = Some(info.shape.clone());
280 parameter.checkpoint_dtype = Some(info.dtype.clone());
281 report.matched += 1;
282 }
283 None => {
284 record_missing(&mut report, parameter, exact);
285 parameter.checkpoint_shape = None;
286 parameter.checkpoint_dtype = None;
287 }
288 }
289 }
290
291 for (index, claimed) in claimed.iter().enumerate() {
292 if !claimed {
293 report.unclaimed.push(names[index].to_string());
294 }
295 }
296 report.missing_certain.sort();
297 report.missing_conditional.sort();
298 report.unclaimed.sort();
299 report
300}
301
302fn record_missing(report: &mut VerifyReport, parameter: &Parameter, key: String) {
303 if missing_is_certain(parameter) {
304 report.missing_certain.push(key);
305 } else {
306 report.missing_conditional.push(key);
307 }
308}
309
310fn missing_is_certain(parameter: &Parameter) -> bool {
311 match parameter.role {
312 ParameterRole::Conditional | ParameterRole::Unknown | ParameterRole::Excluded => false,
313 ParameterRole::Optimized | ParameterRole::Frozen | ParameterRole::RunningState => parameter
314 .evidence
315 .iter()
316 .any(|evidence| evidence.confidence == Confidence::Proven),
317 }
318}