use std::collections::HashSet;
use std::path::Path;
use prost::Message;
use onnx_runtime_ir::{
Attribute, DataType, Dim, Graph, Node, Shape, TensorData, TypeProto, ValueId, WeightRef,
};
use crate::proto::onnx::{
self, attribute_proto::AttributeType, tensor_shape_proto, type_proto, AttributeProto,
GraphProto, ModelProto, NodeProto, OperatorSetIdProto, StringStringEntryProto, TensorProto,
TensorShapeProto, ValueInfoProto,
};
use crate::weights::WeightStore;
use crate::LoaderError;
pub const DEFAULT_IR_VERSION: i64 = 10;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModelMetadata {
pub ir_version: i64,
pub producer_name: String,
pub producer_version: String,
pub domain: String,
pub model_version: i64,
pub doc_string: Option<String>,
pub graph_name: String,
pub metadata_props: Vec<(String, String)>,
}
impl Default for ModelMetadata {
fn default() -> Self {
Self {
ir_version: DEFAULT_IR_VERSION,
producer_name: String::new(),
producer_version: String::new(),
domain: String::new(),
model_version: 0,
doc_string: None,
graph_name: String::new(),
metadata_props: Vec::new(),
}
}
}
pub struct Model<'a> {
pub graph: &'a Graph,
pub metadata: ModelMetadata,
pub weights: Option<&'a WeightStore>,
}
impl<'a> Model<'a> {
pub fn new(graph: &'a Graph) -> Self {
Self {
graph,
metadata: ModelMetadata::default(),
weights: None,
}
}
pub fn with_metadata(mut self, metadata: ModelMetadata) -> Self {
self.metadata = metadata;
self
}
pub fn with_weights(mut self, weights: &'a WeightStore) -> Self {
self.weights = Some(weights);
self
}
}
pub fn encode_model(model: &Model) -> Result<Vec<u8>, LoaderError> {
Ok(encode_model_proto(model)?.encode_to_vec())
}
pub fn write_model(model: &Model, path: impl AsRef<Path>) -> Result<(), LoaderError> {
let bytes = encode_model(model)?;
let path = path.as_ref();
std::fs::write(path, bytes).map_err(|source| LoaderError::Io {
path: path.to_path_buf(),
source,
})
}
pub fn encode_model_proto(model: &Model) -> Result<ModelProto, LoaderError> {
let meta = &model.metadata;
let graph = encode_graph_proto(model.graph, model.weights, true, &meta.graph_name)?;
let mut opset_import: Vec<OperatorSetIdProto> = model
.graph
.opset_imports
.iter()
.map(|(domain, &version)| OperatorSetIdProto {
domain: domain.clone(),
version: version as i64,
})
.collect();
if meta.ir_version >= 3
&& !opset_import
.iter()
.any(|opset| opset.domain.is_empty() || opset.domain == "ai.onnx")
{
opset_import.push(OperatorSetIdProto {
domain: String::new(),
version: 21,
});
}
opset_import.sort_by(|a, b| a.domain.cmp(&b.domain));
let metadata_props = meta
.metadata_props
.iter()
.map(|(key, value)| StringStringEntryProto {
key: key.clone(),
value: value.clone(),
})
.collect();
Ok(ModelProto {
ir_version: meta.ir_version,
opset_import,
producer_name: meta.producer_name.clone(),
producer_version: meta.producer_version.clone(),
domain: meta.domain.clone(),
model_version: meta.model_version,
doc_string: meta.doc_string.clone().unwrap_or_default(),
graph: Some(graph),
metadata_props,
..Default::default()
})
}
fn encode_graph_proto(
graph: &Graph,
weights: Option<&WeightStore>,
_is_top_level: bool,
name: &str,
) -> Result<GraphProto, LoaderError> {
let mut init_ids: Vec<ValueId> = graph.initializers.keys().copied().collect();
init_ids.sort_by_key(|v| v.0);
let mut initializer = Vec::with_capacity(init_ids.len());
for vid in &init_ids {
let weight = &graph.initializers[vid];
let iname = value_name(graph, *vid).unwrap_or_default().to_string();
initializer.push(encode_weight(iname, weight, weights)?);
}
let input: Vec<ValueInfoProto> = graph
.inputs
.iter()
.map(|&vid| encode_value_info(graph, vid))
.collect();
let output: Vec<ValueInfoProto> = graph
.outputs
.iter()
.map(|&vid| encode_value_info(graph, vid))
.collect();
let mut excluded: HashSet<ValueId> = HashSet::new();
excluded.extend(graph.inputs.iter().copied());
excluded.extend(graph.outputs.iter().copied());
excluded.extend(init_ids.iter().copied());
let mut value_info = Vec::new();
for (vid, value) in graph.values.iter() {
if excluded.contains(&vid) {
continue;
}
if value.name.as_deref().is_some_and(|n| !n.is_empty()) {
value_info.push(encode_value_info(graph, vid));
}
}
let mut node = Vec::with_capacity(graph.num_nodes());
for (_, n) in graph.nodes.iter() {
node.push(encode_node(graph, n)?);
}
Ok(GraphProto {
node,
name: name.to_string(),
initializer,
input,
output,
value_info,
..Default::default()
})
}
fn encode_node(graph: &Graph, node: &Node) -> Result<NodeProto, LoaderError> {
let input: Vec<String> = node
.inputs
.iter()
.map(|slot| match slot {
Some(vid) => value_name(graph, *vid).unwrap_or_default().to_string(),
None => String::new(),
})
.collect();
let output: Vec<String> = node
.outputs
.iter()
.map(|&vid| value_name(graph, vid).unwrap_or_default().to_string())
.collect();
let mut keys: Vec<&String> = node.attributes.keys().collect();
keys.sort();
let mut attribute = Vec::with_capacity(keys.len());
for key in keys {
attribute.push(encode_attribute(graph, key, &node.attributes[key])?);
}
Ok(NodeProto {
input,
output,
name: node.name.clone(),
op_type: node.op_type.clone(),
domain: node.domain.clone(),
attribute,
doc_string: node.doc_string.clone().unwrap_or_default(),
..Default::default()
})
}
fn encode_attribute(
graph: &Graph,
name: &str,
attr: &Attribute,
) -> Result<AttributeProto, LoaderError> {
let mut ap = AttributeProto {
name: name.to_string(),
..Default::default()
};
match attr {
Attribute::Int(v) => {
ap.i = *v;
ap.r#type = AttributeType::Int as i32;
}
Attribute::Float(v) => {
ap.f = *v;
ap.r#type = AttributeType::Float as i32;
}
Attribute::String(s) => {
ap.s = s.clone();
ap.r#type = AttributeType::String as i32;
}
Attribute::Ints(v) => {
ap.ints = v.clone();
ap.r#type = AttributeType::Ints as i32;
}
Attribute::Floats(v) => {
ap.floats = v.clone();
ap.r#type = AttributeType::Floats as i32;
}
Attribute::Strings(v) => {
ap.strings = v.clone();
ap.r#type = AttributeType::Strings as i32;
}
Attribute::Tensor(t) => {
ap.t = Some(encode_tensor(t));
ap.r#type = AttributeType::Tensor as i32;
}
Attribute::Graph(_) | Attribute::Graphs(_) => {
return Err(LoaderError::GraphBuild(format!(
"attribute {name:?}: encoding control-flow subgraph attributes is \
unsupported (nested-graph formal I/O is not round-tripped by the \
load path)"
)));
}
Attribute::TypeProto(tp) => {
ap.tp = Some(encode_type_proto(graph, tp));
ap.r#type = AttributeType::TypeProto as i32;
}
Attribute::SparseTensor(_) => {
return Err(LoaderError::GraphBuild(format!(
"attribute {name:?}: SparseTensor encoding is unsupported"
)));
}
}
Ok(ap)
}
fn encode_tensor(t: &TensorData) -> TensorProto {
let mut tp = TensorProto {
dims: t.dims.iter().map(|&d| d as i64).collect(),
data_type: t.dtype.to_onnx(),
name: t.name.clone().unwrap_or_default(),
..Default::default()
};
if t.dtype == DataType::String {
tp.string_data = t.strings.iter().map(|s| s.clone().into_bytes()).collect();
} else {
tp.raw_data = t.data.clone();
}
tp
}
fn encode_weight(
name: String,
weight: &WeightRef,
weights: Option<&WeightStore>,
) -> Result<TensorProto, LoaderError> {
match weight {
WeightRef::Inline(t) => {
let mut tp = encode_tensor(t);
tp.name = name;
Ok(tp)
}
WeightRef::External { dtype, dims, .. } => {
if *dtype == DataType::String {
return Err(LoaderError::GraphBuild(format!(
"external initializer {name:?}: STRING external data is unsupported"
)));
}
let bytes = weights.and_then(|s| s.bytes(weight)).ok_or_else(|| {
LoaderError::GraphBuild(format!(
"external initializer {name:?}: weight bytes unavailable \
(attach a WeightStore via Model::with_weights)"
))
})?;
Ok(TensorProto {
name,
data_type: dtype.to_onnx(),
dims: dims.iter().map(|&d| d as i64).collect(),
raw_data: bytes.to_vec(),
..Default::default()
})
}
}
}
fn encode_value_info(graph: &Graph, vid: ValueId) -> ValueInfoProto {
let value = graph.value(vid);
ValueInfoProto {
name: value.name.clone().unwrap_or_default(),
r#type: Some(encode_tensor_type(graph, value.dtype, &value.shape)),
..Default::default()
}
}
fn encode_tensor_type(graph: &Graph, dtype: DataType, shape: &Shape) -> onnx::TypeProto {
onnx::TypeProto {
value: Some(type_proto::Value::TensorType(type_proto::Tensor {
elem_type: dtype.to_onnx(),
shape: Some(encode_shape(graph, shape)),
})),
..Default::default()
}
}
fn encode_shape(graph: &Graph, shape: &Shape) -> TensorShapeProto {
use tensor_shape_proto::{dimension::Value as DV, Dimension};
let dim = shape
.iter()
.map(|d| {
let value = match d {
Dim::Static(n) => Some(DV::DimValue(*n as i64)),
Dim::Symbolic(sym) => graph
.symbol_constraints
.get(sym)
.and_then(|c| c.name.clone())
.map(DV::DimParam),
};
Dimension {
value,
..Default::default()
}
})
.collect();
TensorShapeProto { dim }
}
fn encode_type_proto(graph: &Graph, tp: &TypeProto) -> onnx::TypeProto {
let value = match tp {
TypeProto::Tensor { dtype, shape } => type_proto::Value::TensorType(type_proto::Tensor {
elem_type: dtype.to_onnx(),
shape: Some(encode_shape(graph, shape)),
}),
TypeProto::SparseTensor { dtype, shape } => {
type_proto::Value::SparseTensorType(type_proto::SparseTensor {
elem_type: dtype.to_onnx(),
shape: Some(encode_shape(graph, shape)),
})
}
TypeProto::Sequence(inner) => type_proto::Value::SequenceType(Box::new(type_proto::Sequence {
elem_type: Some(Box::new(encode_type_proto(graph, inner))),
})),
TypeProto::Optional(inner) => type_proto::Value::OptionalType(Box::new(type_proto::Optional {
elem_type: Some(Box::new(encode_type_proto(graph, inner))),
})),
TypeProto::Map { key, value } => type_proto::Value::MapType(Box::new(type_proto::Map {
key_type: key.to_onnx(),
value_type: Some(Box::new(encode_type_proto(graph, value))),
})),
};
onnx::TypeProto {
value: Some(value),
..Default::default()
}
}
fn value_name(graph: &Graph, vid: ValueId) -> Option<&str> {
graph.try_value(vid).and_then(|v| v.name.as_deref())
}