open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-loader-gguf/examples/load_tflite.rs
Transaction Science f4838f1173 loader-gguf wave 38: TFLite (Google FlatBuffers) reader
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]>
2026-06-09 11:01:25 -04:00

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)
}
}