Pure-Rust hand-rolled FlatBuffers reader, ~250 LoC. No
`flatbuffers` build-time dep. Random-access design: no
parse step — every field is an offset walk through the
mmap.
SCOPE — what wave 38 does
Open + mmap any `.tflite` file. Walk the top-level
Model table:
TfliteModel {
version, // schema_version (typically 3)
description,
subgraphs: Vec<TfliteSubgraph>,
buffers: Vec<TfliteBuffer>,
mmap,
}
TfliteBuffer::
Inline {offset, len} bytes inline in the FlatBuffer
External {offset,size} large-model support (TF 2.13+):
buffer data lives at an
absolute file offset past the
FlatBuffer, allowing >2 GiB
models without overflowing the
internal uoffset_t address
space.
Empty buffer 0 sentinel + I/O tensors
Per-subgraph tensors carry (name, shape, dtype, buffer
index, subgraph index). `tensor_bytes()` resolves the
buffer indirection and returns a slice into the mmap
(or None for External outside the mmap window).
DTYPE COVERAGE
All 18 documented TensorType variants (Float32 through
Int4). Unknown values pass through as Unknown(u8) —
same forward-compat strategy as ONNX.
SCOPE — what wave 38 does NOT do
No operator graph execution — JouleClaw has its own
kernel suite (waves 1-33). TFLite ops are out of
substrate scope. We use TFLite as a tensor archive
format.
No quantization-params decoding (zero point + scale
arrays). The raw flatbuf reader is in-file; future
consumers can pluck those fields with a dozen lines of
field-index lookups.
FLATBUFFERS WIRE READER
FlatBuf struct: u8/u16/u32/u64 reads with absolute-byte
bounds checking. Supports table walking via vtables
(header u16 + inline_size u16 + field_offsets u16[]),
vector reads (length-prefixed), string reads (UTF-8 +
NUL terminator), uoffset_t forward indirection. ~120
LoC.
NEW FILES
crates/jouleclaw-loader-gguf/src/tflite.rs
crates/jouleclaw-loader-gguf/examples/load_tflite.rs
lib.rs: + pub mod tflite;
REAL-MODEL VERIFICATION (MediaPipe)
hair_segmentation.tflite 762 KiB
load: 3 ms wall clock
schema v3, "model-1244_hair_best.tflite.no_meta"
1 subgraph, 609 tensors, all Float32
349 buffers, 709 KiB inline weights
ssdlite_object_detection.tflite 5.96 MiB (MobileNetV2-SSDLite)
304 tensors: 291 Float32 + 13 Int32
5.92 MiB inline weights
Tensor names like "BoxPredictor_1/ClassPredictor/weights/read"
surface intact with correct buffer indices.
TESTS (4 new)
dtype_element_sizes f16/f32/f64/i*/u* sizing
dtype_handles_unknown forward-compat fallback
too_short_file_returns_error header bounds
out_of_bounds_offset_returns_error nonsense root offset
loader-gguf: 189 → 193 tests.
Synthetic FlatBuffer round-trip test deferred — canonical
FlatBuffers requires parent-first / child-last layout
with patch-up of uoffsets; hand-rolling a correct builder
is more code than the reader. Real-model coverage via
`examples/load_tflite.rs` is the substrate validation
path.
NEXT — Wave 39: OpenVINO IR (XML + .bin sibling).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
137 lines
4.5 KiB
Rust
137 lines
4.5 KiB
Rust
//! Real-model bring-up for TFLite files.
|
|
//!
|
|
//! ```bash
|
|
//! cargo run --release --example load_tflite -- path/to/model.tflite
|
|
//! ```
|
|
|
|
use jouleclaw_loader_gguf::tflite::{read_tflite_file, TfliteBuffer, TfliteDtype};
|
|
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_tflite <path.tflite>");
|
|
std::process::exit(1);
|
|
}
|
|
let path = PathBuf::from(&args[1]);
|
|
println!("model: {}", path.display());
|
|
let t0 = Instant::now();
|
|
let m = read_tflite_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!(" flatbuffer_schema_version: {}", m.version);
|
|
if !m.description.is_empty() {
|
|
println!(" description: {}", m.description);
|
|
}
|
|
println!(" subgraph_count: {}", m.subgraphs.len());
|
|
println!(" buffer_count: {}", m.buffers.len());
|
|
println!(" mmap_len: {} ({})", m.mmap.len(), humanize_bytes(m.mmap.len() as u64));
|
|
|
|
// Buffer-class accounting.
|
|
let mut inline_bytes: u64 = 0;
|
|
let mut external_bytes: u64 = 0;
|
|
let mut empty = 0u32;
|
|
for b in &m.buffers {
|
|
match b {
|
|
TfliteBuffer::Inline { len, .. } => inline_bytes += *len as u64,
|
|
TfliteBuffer::External { size, .. } => external_bytes += *size,
|
|
TfliteBuffer::Empty => empty += 1,
|
|
}
|
|
}
|
|
println!();
|
|
println!("=== buffer storage ===");
|
|
println!(" inline_bytes: {} ({})", inline_bytes, humanize_bytes(inline_bytes));
|
|
println!(" external_bytes: {} ({})", external_bytes, humanize_bytes(external_bytes));
|
|
println!(" empty_buffers: {}", empty);
|
|
|
|
// Per-subgraph summary.
|
|
println!();
|
|
println!("=== subgraphs ===");
|
|
for (i, sg) in m.subgraphs.iter().enumerate() {
|
|
let name = if sg.name.is_empty() { "(unnamed)" } else { &sg.name };
|
|
println!(
|
|
" [{:>2}] {} — tensors={} inputs={} outputs={}",
|
|
i, name, sg.tensors.len(), sg.inputs.len(), sg.outputs.len()
|
|
);
|
|
}
|
|
|
|
// Dtype distribution across ALL tensors.
|
|
let mut counts: BTreeMap<String, (u32, u64)> = BTreeMap::new();
|
|
for sg in &m.subgraphs {
|
|
for t in &sg.tensors {
|
|
let key = format!("{:?}", t.dtype);
|
|
let bytes = match m.buffers.get(t.buffer) {
|
|
Some(TfliteBuffer::Inline { len, .. }) => *len as u64,
|
|
Some(TfliteBuffer::External { size, .. }) => *size,
|
|
_ => 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 10 tensors by buffer size.
|
|
let mut all_tensors: Vec<_> = m.subgraphs
|
|
.iter()
|
|
.flat_map(|sg| sg.tensors.iter())
|
|
.collect();
|
|
all_tensors.sort_by_key(|t| {
|
|
std::cmp::Reverse(match m.buffers.get(t.buffer) {
|
|
Some(TfliteBuffer::Inline { len, .. }) => *len as u64,
|
|
Some(TfliteBuffer::External { size, .. }) => *size,
|
|
_ => 0,
|
|
})
|
|
});
|
|
println!();
|
|
println!("=== top 10 tensors by buffer size ===");
|
|
for t in all_tensors.iter().take(10) {
|
|
let bytes = match m.buffers.get(t.buffer) {
|
|
Some(TfliteBuffer::Inline { len, .. }) => *len as u64,
|
|
Some(TfliteBuffer::External { size, .. }) => *size,
|
|
_ => 0,
|
|
};
|
|
println!(
|
|
" {:?} {:<60} {:>14} ({})",
|
|
t.dtype,
|
|
truncate(&t.name, 60),
|
|
bytes,
|
|
humanize_bytes(bytes),
|
|
);
|
|
let _ = TfliteDtype::Float32; // silence unused-import
|
|
}
|
|
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)
|
|
}
|
|
}
|