1use std::path::Path;
13
14use anyhow::{bail, Context, Result};
15
16use crate::cast::{f32_to_bytes, SrcDtype};
17use crate::read::{Checkpoint, RawTensor};
18
19const DT_FLOAT: i32 = 1;
21const DT_UINT8: i32 = 2;
22const DT_INT8: i32 = 3;
23const DT_UINT16: i32 = 4;
24const DT_INT16: i32 = 5;
25const DT_INT32: i32 = 6;
26const DT_INT64: i32 = 7;
27const DT_BOOL: i32 = 9;
28const DT_FLOAT16: i32 = 10;
29const DT_DOUBLE: i32 = 11;
30const DT_UINT32: i32 = 12;
31const DT_UINT64: i32 = 13;
32const DT_BFLOAT16: i32 = 16;
33
34const LOCATION_EXTERNAL: u64 = 1;
35
36pub fn load_onnx(path: &Path) -> Result<Checkpoint> {
37 const MAX_ONNX_BYTES: u64 = 8 << 30;
38 const MAX_INITIALIZERS: usize = 100_000;
39 let meta = std::fs::metadata(path).with_context(|| format!("stat {}", path.display()))?;
40 anyhow::ensure!(
41 meta.len() <= MAX_ONNX_BYTES,
42 "ONNX file too large: {} bytes (max {MAX_ONNX_BYTES})",
43 meta.len()
44 );
45 let bytes = std::fs::read(path).with_context(|| format!("read {}", path.display()))?;
46 let initializers = parse_model(&bytes).context("parse ONNX protobuf")?;
47 anyhow::ensure!(
48 !initializers.is_empty(),
49 "ONNX graph has no initializer tensors — the weights may live in external \
50 data files or be supplied as runtime inputs"
51 );
52 anyhow::ensure!(
53 initializers.len() <= MAX_INITIALIZERS,
54 "ONNX has too many initializers: {} (max {MAX_INITIALIZERS})",
55 initializers.len()
56 );
57
58 let mut tensors = Vec::with_capacity(initializers.len());
59 for t in &initializers {
60 tensors.push(tensor_proto_to_raw(t)?);
61 }
62 Ok(Checkpoint {
63 tensors,
64 metadata: Vec::new(),
65 })
66}
67
68struct ProtoReader<'a> {
73 buf: &'a [u8],
74 pos: usize,
75}
76
77impl<'a> ProtoReader<'a> {
78 fn new(buf: &'a [u8]) -> Self {
79 Self { buf, pos: 0 }
80 }
81
82 fn done(&self) -> bool {
83 self.pos >= self.buf.len()
84 }
85
86 fn read_varint(&mut self) -> Result<u64> {
87 let mut value = 0u64;
88 let mut shift = 0u32;
89 loop {
90 let byte = *self
91 .buf
92 .get(self.pos)
93 .context("protobuf: truncated varint")?;
94 self.pos += 1;
95 if shift < 64 {
96 value |= u64::from(byte & 0x7F) << shift;
97 }
98 if byte & 0x80 == 0 {
99 return Ok(value);
100 }
101 shift += 7;
102 anyhow::ensure!(shift <= 70, "protobuf: varint too long");
103 }
104 }
105
106 fn read_tag(&mut self) -> Result<(u64, u8)> {
108 let tag = self.read_varint()?;
109 Ok((tag >> 3, (tag & 0x7) as u8))
110 }
111
112 fn read_len_delimited(&mut self) -> Result<&'a [u8]> {
113 let len = self.read_varint()? as usize;
114 let end = self
115 .pos
116 .checked_add(len)
117 .filter(|&e| e <= self.buf.len())
118 .context("protobuf: truncated length-delimited field")?;
119 let slice = &self.buf[self.pos..end];
120 self.pos = end;
121 Ok(slice)
122 }
123
124 fn read_fixed32(&mut self) -> Result<u32> {
125 let end = self.pos + 4;
126 anyhow::ensure!(end <= self.buf.len(), "protobuf: truncated fixed32");
127 let v = u32::from_le_bytes(self.buf[self.pos..end].try_into().unwrap());
128 self.pos = end;
129 Ok(v)
130 }
131
132 fn read_fixed64(&mut self) -> Result<u64> {
133 let end = self.pos + 8;
134 anyhow::ensure!(end <= self.buf.len(), "protobuf: truncated fixed64");
135 let v = u64::from_le_bytes(self.buf[self.pos..end].try_into().unwrap());
136 self.pos = end;
137 Ok(v)
138 }
139
140 fn skip(&mut self, wire_type: u8) -> Result<()> {
141 match wire_type {
142 0 => {
143 self.read_varint()?;
144 }
145 1 => {
146 self.read_fixed64()?;
147 }
148 2 => {
149 self.read_len_delimited()?;
150 }
151 5 => {
152 self.read_fixed32()?;
153 }
154 other => bail!("protobuf: unsupported wire type {other}"),
155 }
156 Ok(())
157 }
158}
159
160#[derive(Default)]
161struct TensorProto {
162 dims: Vec<i64>,
163 data_type: i32,
164 float_data: Vec<f32>,
165 int32_data: Vec<i32>,
166 int64_data: Vec<i64>,
167 uint64_data: Vec<u64>,
168 double_data: Vec<f64>,
169 name: String,
170 raw_data: Vec<u8>,
171 data_location: u64,
172}
173
174fn parse_model(buf: &[u8]) -> Result<Vec<TensorProto>> {
176 let mut r = ProtoReader::new(buf);
177 let mut initializers = Vec::new();
178 while !r.done() {
179 let (field, wire) = r.read_tag()?;
180 if field == 7 && wire == 2 {
181 let graph = r.read_len_delimited()?;
182 initializers.extend(parse_graph(graph)?);
183 } else {
184 r.skip(wire)?;
185 }
186 }
187 Ok(initializers)
188}
189
190fn parse_graph(buf: &[u8]) -> Result<Vec<TensorProto>> {
192 let mut r = ProtoReader::new(buf);
193 let mut initializers = Vec::new();
194 while !r.done() {
195 let (field, wire) = r.read_tag()?;
196 if field == 5 && wire == 2 {
197 let tensor = r.read_len_delimited()?;
198 initializers.push(parse_tensor(tensor)?);
199 } else {
200 r.skip(wire)?;
201 }
202 }
203 Ok(initializers)
204}
205
206fn parse_tensor(buf: &[u8]) -> Result<TensorProto> {
207 let mut r = ProtoReader::new(buf);
208 let mut t = TensorProto::default();
209 while !r.done() {
210 let (field, wire) = r.read_tag()?;
211 match (field, wire) {
212 (1, 0) => t.dims.push(r.read_varint()? as i64),
214 (1, 2) => {
215 let mut p = ProtoReader::new(r.read_len_delimited()?);
216 while !p.done() {
217 t.dims.push(p.read_varint()? as i64);
218 }
219 }
220 (2, 0) => t.data_type = r.read_varint()? as i32,
221 (4, 5) => t.float_data.push(f32::from_bits(r.read_fixed32()?)),
223 (4, 2) => {
224 let mut p = ProtoReader::new(r.read_len_delimited()?);
225 while !p.done() {
226 t.float_data.push(f32::from_bits(p.read_fixed32()?));
227 }
228 }
229 (5, 0) => t.int32_data.push(r.read_varint()? as i64 as i32),
231 (5, 2) => {
232 let mut p = ProtoReader::new(r.read_len_delimited()?);
233 while !p.done() {
234 t.int32_data.push(p.read_varint()? as i64 as i32);
235 }
236 }
237 (7, 0) => t.int64_data.push(r.read_varint()? as i64),
239 (7, 2) => {
240 let mut p = ProtoReader::new(r.read_len_delimited()?);
241 while !p.done() {
242 t.int64_data.push(p.read_varint()? as i64);
243 }
244 }
245 (8, 2) => {
246 t.name = String::from_utf8_lossy(r.read_len_delimited()?).into_owned();
247 }
248 (9, 2) => t.raw_data = r.read_len_delimited()?.to_vec(),
249 (10, 1) => t.double_data.push(f64::from_bits(r.read_fixed64()?)),
251 (10, 2) => {
252 let mut p = ProtoReader::new(r.read_len_delimited()?);
253 while !p.done() {
254 t.double_data.push(f64::from_bits(p.read_fixed64()?));
255 }
256 }
257 (11, 0) => t.uint64_data.push(r.read_varint()?),
259 (11, 2) => {
260 let mut p = ProtoReader::new(r.read_len_delimited()?);
261 while !p.done() {
262 t.uint64_data.push(p.read_varint()?);
263 }
264 }
265 (14, 0) => t.data_location = r.read_varint()?,
266 (_, w) => r.skip(w)?,
267 }
268 }
269 Ok(t)
270}
271
272fn tensor_proto_to_raw(t: &TensorProto) -> Result<RawTensor> {
277 let name = t.name.clone();
278 if t.data_location == LOCATION_EXTERNAL {
279 bail!(
280 "tensor {name}: stored as external data — re-export the model with weights \
281 embedded (onnx.external_data_helper.load_external_data_for_model + save) \
282 and convert again"
283 );
284 }
285 let shape: Vec<u64> = t.dims.iter().map(|&d| d as u64).collect();
286
287 let (dtype, data): (SrcDtype, Vec<u8>) = match t.data_type {
288 DT_FLOAT => {
289 if !t.raw_data.is_empty() {
290 (SrcDtype::F32, t.raw_data.clone())
291 } else {
292 (SrcDtype::F32, f32_to_bytes(&t.float_data))
293 }
294 }
295 DT_FLOAT16 | DT_BFLOAT16 => {
296 let dtype = if t.data_type == DT_FLOAT16 {
297 SrcDtype::F16
298 } else {
299 SrcDtype::BF16
300 };
301 if !t.raw_data.is_empty() {
302 (dtype, t.raw_data.clone())
303 } else {
304 let bytes = t
307 .int32_data
308 .iter()
309 .flat_map(|&v| (v as u16).to_le_bytes())
310 .collect();
311 (dtype, bytes)
312 }
313 }
314 DT_DOUBLE => {
315 eprintln!("note: tensor {name}: casting DOUBLE to F32");
316 let vals: Vec<f32> = if !t.raw_data.is_empty() {
317 t.raw_data
318 .chunks_exact(8)
319 .map(|c| f64::from_le_bytes(c.try_into().unwrap()) as f32)
320 .collect()
321 } else {
322 t.double_data.iter().map(|&v| v as f32).collect()
323 };
324 (SrcDtype::F32, f32_to_bytes(&vals))
325 }
326 DT_INT64 => {
327 eprintln!("note: tensor {name}: casting INT64 to F32");
328 let vals: Vec<f32> = if !t.raw_data.is_empty() {
329 t.raw_data
330 .chunks_exact(8)
331 .map(|c| i64::from_le_bytes(c.try_into().unwrap()) as f32)
332 .collect()
333 } else {
334 t.int64_data.iter().map(|&v| v as f32).collect()
335 };
336 (SrcDtype::F32, f32_to_bytes(&vals))
337 }
338 DT_UINT64 => {
339 eprintln!("note: tensor {name}: casting UINT64 to F32");
340 let vals: Vec<f32> = if !t.raw_data.is_empty() {
341 t.raw_data
342 .chunks_exact(8)
343 .map(|c| u64::from_le_bytes(c.try_into().unwrap()) as f32)
344 .collect()
345 } else {
346 t.uint64_data.iter().map(|&v| v as f32).collect()
347 };
348 (SrcDtype::F32, f32_to_bytes(&vals))
349 }
350 DT_INT32 | DT_UINT32 | DT_INT16 | DT_UINT16 | DT_INT8 | DT_UINT8 | DT_BOOL => {
351 eprintln!("note: tensor {name}: casting integer/bool data to F32");
352 let vals: Vec<f32> = if !t.raw_data.is_empty() {
353 decode_small_ints(&t.raw_data, t.data_type)?
354 } else {
355 t.int32_data.iter().map(|&v| v as f32).collect()
357 };
358 (SrcDtype::F32, f32_to_bytes(&vals))
359 }
360 other => bail!("tensor {name}: ONNX data_type {other} not supported"),
361 };
362
363 Ok(RawTensor {
364 name,
365 shape,
366 dtype,
367 data,
368 })
369}
370
371fn decode_small_ints(raw: &[u8], data_type: i32) -> Result<Vec<f32>> {
372 Ok(match data_type {
373 DT_INT32 => raw
374 .chunks_exact(4)
375 .map(|c| i32::from_le_bytes(c.try_into().unwrap()) as f32)
376 .collect(),
377 DT_UINT32 => raw
378 .chunks_exact(4)
379 .map(|c| u32::from_le_bytes(c.try_into().unwrap()) as f32)
380 .collect(),
381 DT_INT16 => raw
382 .chunks_exact(2)
383 .map(|c| i16::from_le_bytes(c.try_into().unwrap()) as f32)
384 .collect(),
385 DT_UINT16 => raw
386 .chunks_exact(2)
387 .map(|c| u16::from_le_bytes(c.try_into().unwrap()) as f32)
388 .collect(),
389 DT_INT8 => raw.iter().map(|&b| b as i8 as f32).collect(),
390 DT_UINT8 => raw.iter().map(|&b| b as f32).collect(),
391 DT_BOOL => raw.iter().map(|&b| (b != 0) as u8 as f32).collect(),
392 other => bail!("unexpected small-int data_type {other}"),
393 })
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 fn varint(mut v: u64, out: &mut Vec<u8>) {
402 loop {
403 let byte = (v & 0x7F) as u8;
404 v >>= 7;
405 if v == 0 {
406 out.push(byte);
407 break;
408 }
409 out.push(byte | 0x80);
410 }
411 }
412
413 fn tag(field: u64, wire: u8, out: &mut Vec<u8>) {
414 varint(field << 3 | wire as u64, out);
415 }
416
417 fn len_delim(field: u64, payload: &[u8], out: &mut Vec<u8>) {
418 tag(field, 2, out);
419 varint(payload.len() as u64, out);
420 out.extend_from_slice(payload);
421 }
422
423 fn build_model(tensor: &[u8]) -> Vec<u8> {
424 let mut graph = Vec::new();
425 len_delim(5, tensor, &mut graph); let mut model = Vec::new();
427 tag(1, 0, &mut model); varint(8, &mut model);
429 len_delim(7, &graph, &mut model); model
431 }
432
433 #[test]
434 fn parses_f32_raw_data_tensor() {
435 let vals = [1.5f32, -2.25, 3.0];
436 let mut t = Vec::new();
437 let mut dims = Vec::new();
439 varint(3, &mut dims);
440 len_delim(1, &dims, &mut t);
441 tag(2, 0, &mut t); varint(DT_FLOAT as u64, &mut t);
443 len_delim(8, b"w", &mut t); let raw: Vec<u8> = vals.iter().flat_map(|v| v.to_le_bytes()).collect();
445 len_delim(9, &raw, &mut t); let model = build_model(&t);
448 let init = parse_model(&model).unwrap();
449 assert_eq!(init.len(), 1);
450 let rt = tensor_proto_to_raw(&init[0]).unwrap();
451 assert_eq!(rt.name, "w");
452 assert_eq!(rt.shape, vec![3]);
453 assert_eq!(rt.dtype, SrcDtype::F32);
454 let got: Vec<f32> = rt
455 .data
456 .chunks_exact(4)
457 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
458 .collect();
459 assert_eq!(got, vals);
460 }
461
462 #[test]
463 fn parses_packed_float_data_and_int64_dims_unpacked() {
464 let mut t = Vec::new();
465 tag(1, 0, &mut t); varint(2, &mut t);
467 tag(1, 0, &mut t); varint(2, &mut t);
469 tag(2, 0, &mut t);
470 varint(DT_FLOAT as u64, &mut t);
471 len_delim(8, b"packed", &mut t);
472 let mut fd = Vec::new();
473 for v in [0.5f32, 1.0, 1.5, 2.0] {
474 fd.extend_from_slice(&v.to_le_bytes());
475 }
476 len_delim(4, &fd, &mut t); let init = parse_model(&build_model(&t)).unwrap();
479 let rt = tensor_proto_to_raw(&init[0]).unwrap();
480 assert_eq!(rt.shape, vec![2, 2]);
481 assert_eq!(rt.data.len(), 16);
482 }
483
484 #[test]
485 fn rejects_external_data() {
486 let mut t = Vec::new();
487 tag(2, 0, &mut t);
488 varint(DT_FLOAT as u64, &mut t);
489 len_delim(8, b"ext", &mut t);
490 tag(14, 0, &mut t); varint(LOCATION_EXTERNAL, &mut t);
492
493 let init = parse_model(&build_model(&t)).unwrap();
494 let err = match tensor_proto_to_raw(&init[0]) {
495 Ok(_) => panic!("expected external-data error"),
496 Err(e) => e,
497 };
498 assert!(err.to_string().contains("external data"));
499 }
500}