Pure-Rust hand-rolled proto3 wire-format reader, ~400 LoC.
No `prost` build-time dep, no operator graph execution.
SCOPE — what wave 36 does
Open + mmap any .onnx file.
Walk ModelProto → GraphProto → repeated TensorProto
initializer entries. Surface each as:
OnnxTensorInfo {
name: String,
dtype: OnnxDtype,
shape: Vec<i64>,
data: OnnxTensorData,
}
OnnxTensorData::
RawData{offset, len} most common; bytes inline in
the mmap (the `raw_data` field).
TypedArray(arrays) older / smaller models that
store weights in the typed
repeated fields (float_data /
int32_data / int64_data /
uint64_data / double_data).
External{location,..} >2GB models that shard weights
to sibling files. Surfaced;
caller resolves the file.
SCOPE — what wave 36 does NOT do
No operator graph execution — JouleClaw has its own
kernel suite (waves 1-33). ONNX ops are out of substrate
scope. We use ONNX as a tensor archive format.
No protobuf reflection / message descriptor parsing —
we hard-code the 6 field tags we care about (ModelProto
fields 1/2/7/14, GraphProto field 5, TensorProto fields
1/2/3/4/7/8/9/10/11/13). Future ONNX dtype additions
pass through transparently as OnnxDtype::Unknown(u32).
DTYPE COVERAGE
All 23 documented TensorProto::DataType variants
(Float through Float4E2M1 + sub-byte Uint4/Int4 + all
four FP8 flavours). Unknowns pass through, don't fail
the parse.
PROTO3 WIRE READER
ProtoReader handles wire types 0/1/2/5 — varint,
64-bit, length-delimited, 32-bit. Hard-fails on unknown
wire types with absolute-byte error offsets. Supports
both packed and unpacked repeated fields. ~120 LoC.
NEW FILES
crates/jouleclaw-loader-gguf/src/onnx.rs
crates/jouleclaw-loader-gguf/examples/load_onnx.rs
lib.rs: + pub mod onnx;
TESTS (7 new)
varint_round_trip_small_and_large canonical varints
read_single_raw_tensor full round-trip
read_multiple_tensors multi-dtype lookup
dtype_round_trip_handles_unknown forward-compat
empty_model_parses_no_error zero-init graphs
dtype_element_sizes sub-byte sentinel
truncated_proto_returns_error bounds checking
loader-gguf: 177 → 184 tests.
cargo check --workspace --all-targets clean.
cargo build --example load_onnx clean.
NEXT — Wave 37: CoreML mlpackage (ZIP of mlmodel +
weight.bin + Metal shaders).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
159 lines
5.3 KiB
Rust
159 lines
5.3 KiB
Rust
//! Real-model bring-up for ONNX files.
|
|
//!
|
|
//! ```bash
|
|
//! cargo run --release --example load_onnx -- path/to/model.onnx
|
|
//! ```
|
|
|
|
use jouleclaw_loader_gguf::onnx::{
|
|
read_onnx_file, OnnxDtype, OnnxTensorData,
|
|
};
|
|
use std::collections::BTreeMap;
|
|
use std::path::PathBuf;
|
|
use std::time::Instant;
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let args: Vec<String> = std::env::args().collect();
|
|
if args.len() < 2 {
|
|
eprintln!("usage: load_onnx <path.onnx>");
|
|
std::process::exit(1);
|
|
}
|
|
let path = PathBuf::from(&args[1]);
|
|
println!("model: {}", path.display());
|
|
let t0 = Instant::now();
|
|
let m = read_onnx_file(&path).map_err(|e| format!("parse: {:?}", e))?;
|
|
let load_secs = t0.elapsed().as_secs_f64();
|
|
|
|
println!();
|
|
println!("=== load ===");
|
|
println!(" load_wall_clock_secs: {:.3}", load_secs);
|
|
println!(" ir_version: {}", m.ir_version);
|
|
if let Some(p) = &m.producer_name {
|
|
println!(" producer: {}", p);
|
|
}
|
|
println!(" tensor_count: {}", m.tensors.len());
|
|
println!(" mmap_len: {} ({})", m.mmap.len(), humanize_bytes(m.mmap.len() as u64));
|
|
|
|
// Byte accounting.
|
|
let mut raw_bytes: u64 = 0;
|
|
let mut typed_bytes: u64 = 0;
|
|
let mut external_count = 0u32;
|
|
let mut external_bytes: u64 = 0;
|
|
for t in &m.tensors {
|
|
match &t.data {
|
|
OnnxTensorData::RawData { len, .. } => raw_bytes += *len as u64,
|
|
OnnxTensorData::TypedArray(a) => {
|
|
typed_bytes += (a.float_data.len() * 4
|
|
+ a.int32_data.len() * 4
|
|
+ a.int64_data.len() * 8
|
|
+ a.uint64_data.len() * 8
|
|
+ a.double_data.len() * 8) as u64;
|
|
}
|
|
OnnxTensorData::External { length, .. } => {
|
|
external_count += 1;
|
|
external_bytes += length.unwrap_or(0);
|
|
}
|
|
}
|
|
}
|
|
println!();
|
|
println!("=== tensor data ===");
|
|
println!(" raw_data_bytes: {} ({})", raw_bytes, humanize_bytes(raw_bytes));
|
|
println!(" typed_array_bytes:{} ({})", typed_bytes, humanize_bytes(typed_bytes));
|
|
println!(
|
|
" external: {} tensors, {} ({} on disk in sibling files)",
|
|
external_count,
|
|
external_bytes,
|
|
humanize_bytes(external_bytes),
|
|
);
|
|
|
|
// Dtype distribution.
|
|
let mut counts: BTreeMap<String, (u32, u64)> = BTreeMap::new();
|
|
for t in &m.tensors {
|
|
let key = format!("{:?}", t.dtype);
|
|
let bytes = match (&t.data, t.dtype.element_size()) {
|
|
(OnnxTensorData::RawData { len, .. }, _) => *len as u64,
|
|
(OnnxTensorData::External { length, .. }, _) => length.unwrap_or(0),
|
|
(OnnxTensorData::TypedArray(_), Some(es)) => {
|
|
(t.n_elements() as u64) * (es as u64)
|
|
}
|
|
(OnnxTensorData::TypedArray(_), None) => 0,
|
|
};
|
|
let e = counts.entry(key).or_insert((0, 0));
|
|
e.0 += 1;
|
|
e.1 += bytes;
|
|
}
|
|
println!();
|
|
println!("=== dtype distribution ===");
|
|
for (k, (c, b)) in &counts {
|
|
println!(" {:<14} count={:>5} size={}", k, c, humanize_bytes(*b));
|
|
}
|
|
|
|
// Top tensors by size.
|
|
let mut sorted: Vec<&_> = m.tensors.iter().collect();
|
|
sorted.sort_by_key(|t| {
|
|
std::cmp::Reverse(match &t.data {
|
|
OnnxTensorData::RawData { len, .. } => *len as u64,
|
|
OnnxTensorData::External { length, .. } => length.unwrap_or(0),
|
|
OnnxTensorData::TypedArray(_) => {
|
|
(t.n_elements() as u64) * (t.dtype.element_size().unwrap_or(0) as u64)
|
|
}
|
|
})
|
|
});
|
|
println!();
|
|
println!("=== top 10 tensors by byte size ===");
|
|
for t in sorted.iter().take(10) {
|
|
let bytes = match &t.data {
|
|
OnnxTensorData::RawData { len, .. } => *len as u64,
|
|
OnnxTensorData::External { length, .. } => length.unwrap_or(0),
|
|
OnnxTensorData::TypedArray(_) => {
|
|
(t.n_elements() as u64) * (t.dtype.element_size().unwrap_or(0) as u64)
|
|
}
|
|
};
|
|
let kind = match &t.data {
|
|
OnnxTensorData::RawData { .. } => "raw",
|
|
OnnxTensorData::TypedArray(_) => "typed",
|
|
OnnxTensorData::External { .. } => "extern",
|
|
};
|
|
println!(
|
|
" [{:>6}] {:?} {:<60} {:>14} ({})",
|
|
kind,
|
|
t.dtype,
|
|
truncate(&t.name, 60),
|
|
bytes,
|
|
humanize_bytes(bytes),
|
|
);
|
|
let _ = OnnxDtype::Float; // silence unused-import in some build configs
|
|
}
|
|
|
|
if !m.metadata.is_empty() {
|
|
println!();
|
|
println!("=== metadata ===");
|
|
for (k, v) in &m.metadata {
|
|
let v_display = if v.len() > 80 { format!("{}…", &v[..80]) } else { v.clone() };
|
|
println!(" {}: {}", k, v_display);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn truncate(s: &str, max: usize) -> String {
|
|
if s.len() <= max {
|
|
s.to_string()
|
|
} else {
|
|
format!("…{}", &s[s.len() - max + 1..])
|
|
}
|
|
}
|
|
|
|
fn humanize_bytes(n: u64) -> String {
|
|
const KB: u64 = 1024;
|
|
const MB: u64 = KB * 1024;
|
|
const GB: u64 = MB * 1024;
|
|
if n >= GB {
|
|
format!("{:.2} GiB", n as f64 / GB as f64)
|
|
} else if n >= MB {
|
|
format!("{:.2} MiB", n as f64 / MB as f64)
|
|
} else if n >= KB {
|
|
format!("{:.2} KiB", n as f64 / KB as f64)
|
|
} else {
|
|
format!("{} B", n)
|
|
}
|
|
}
|