1use std::fmt::Write as _;
21
22use onnx_runtime_ir::{Attribute, DataType, Dim, Graph, NodeId, Shape, ValueId, WeightRef};
23
24use crate::model::Model;
25
26#[derive(Clone, Debug)]
28pub struct PrintOptions {
29 pub indent: String,
31 pub weight_shapes_only: bool,
34 pub doc_strings: bool,
36}
37
38impl Default for PrintOptions {
39 fn default() -> Self {
40 Self {
41 indent: " ".to_string(),
42 weight_shapes_only: true,
43 doc_strings: false,
44 }
45 }
46}
47
48pub fn to_text(model: &Model) -> String {
50 to_text_with(model, &PrintOptions::default())
51}
52
53pub fn to_text_with(model: &Model, opts: &PrintOptions) -> String {
55 let mut out = String::new();
56 let meta = &model.metadata;
57
58 out.push_str("<\n");
60 let _ = writeln!(out, "{}ir_version: {},", opts.indent, meta.ir_version);
61 let mut imports: Vec<(&String, &u64)> = model.graph.opset_imports.iter().collect();
62 imports.sort_by(|a, b| a.0.cmp(b.0));
63 let imports_str = imports
64 .iter()
65 .map(|(domain, version)| format!("{:?} : {}", domain, version))
66 .collect::<Vec<_>>()
67 .join(", ");
68 let _ = writeln!(out, "{}opset_import: [{}]", opts.indent, imports_str);
69 out.push_str(">\n");
70
71 let graph_name = if meta.graph_name.is_empty() {
72 "main"
73 } else {
74 &meta.graph_name
75 };
76 print_graph(&mut out, &model.graph, graph_name, 0, opts);
77 if let Some(proto) = model.retained_proto() {
78 super::extensions::append(proto, &mut out);
79 }
80 out
81}
82
83fn print_graph(out: &mut String, graph: &Graph, name: &str, depth: usize, opts: &PrintOptions) {
85 let pad = opts.indent.repeat(depth);
86 let inner = opts.indent.repeat(depth + 1);
87
88 let inputs = graph
89 .inputs
90 .iter()
91 .map(|&v| typed_value(graph, v))
92 .collect::<Vec<_>>()
93 .join(", ");
94 let outputs = graph
95 .outputs
96 .iter()
97 .map(|&v| typed_value(graph, v))
98 .collect::<Vec<_>>()
99 .join(", ");
100
101 let _ = writeln!(out, "{}{} ({}) => ({}) {{", pad, name, inputs, outputs);
102
103 if opts.weight_shapes_only && !graph.initializers.is_empty() {
105 let _ = writeln!(out, "{}// initializers", inner);
106 let mut inits: Vec<(&ValueId, &WeightRef)> = graph.initializers.iter().collect();
107 inits.sort_by_key(|(v, _)| v.0);
108 for (vid, weight) in inits {
109 let _ = writeln!(
110 out,
111 "{}// {} {} = <{} data omitted>",
112 inner,
113 weight_type(weight),
114 value_name(graph, *vid),
115 weight_kind(weight),
116 );
117 }
118 }
119
120 let order = graph
123 .topological_order()
124 .unwrap_or_else(|_| graph.nodes.keys().collect());
125 for nid in order {
126 print_node(out, graph, nid, depth + 1, opts);
127 }
128
129 let _ = writeln!(out, "{}}}", pad);
130}
131
132fn print_node(out: &mut String, graph: &Graph, nid: NodeId, depth: usize, opts: &PrintOptions) {
134 let pad = opts.indent.repeat(depth);
135 let node = graph.node(nid);
136
137 let outputs = node
138 .outputs
139 .iter()
140 .map(|&v| value_name(graph, v))
141 .collect::<Vec<_>>()
142 .join(", ");
143
144 let op = if node.domain.is_empty() || node.domain == "ai.onnx" {
146 node.op_type.clone()
147 } else {
148 format!("{}.{}", node.domain, node.op_type)
149 };
150
151 let mut inline_attrs: Vec<(&String, &Attribute)> = node
154 .attributes
155 .iter()
156 .filter(|(_, a)| !is_subgraph_attr(a))
157 .collect();
158 inline_attrs.sort_by(|a, b| a.0.cmp(b.0));
159 let attr_str = if inline_attrs.is_empty() {
160 String::new()
161 } else {
162 let body = inline_attrs
163 .iter()
164 .map(|(k, v)| format!("{} = {}", k, attr_value(v)))
165 .collect::<Vec<_>>()
166 .join(", ");
167 format!(" <{}>", body)
168 };
169
170 let inputs = node
171 .inputs
172 .iter()
173 .map(|slot| match slot {
174 Some(v) => value_name(graph, *v),
175 None => "".to_string(),
176 })
177 .collect::<Vec<_>>()
178 .join(", ");
179
180 let doc = if opts.doc_strings {
181 match &node.doc_string {
182 Some(d) if !d.is_empty() => format!(" // {}", d),
183 _ => String::new(),
184 }
185 } else {
186 String::new()
187 };
188
189 let lhs = if outputs.is_empty() {
190 String::new()
191 } else {
192 format!("{} = ", outputs)
193 };
194 let _ = writeln!(out, "{}{}{}{}({}){}", pad, lhs, op, attr_str, inputs, doc);
195
196 let mut subgraph_attrs: Vec<&String> = node
198 .attributes
199 .iter()
200 .filter(|(_, a)| is_subgraph_attr(a))
201 .map(|(k, _)| k)
202 .collect();
203 subgraph_attrs.sort();
204 for attr_name in subgraph_attrs {
205 match &node.attributes[attr_name] {
206 Attribute::Graph(inline) => {
207 let sub = graph
208 .subgraphs
209 .get(&(nid, attr_name.clone()))
210 .unwrap_or(inline);
211 let _ = writeln!(out, "{}{} = graph", pad, attr_name);
212 print_graph(out, sub, "", depth, opts);
213 }
214 Attribute::Graphs(inline) => {
215 for (index, fallback) in inline.iter().enumerate() {
216 let indexed_name = format!("{attr_name}[{index}]");
217 let sub = graph
218 .subgraphs
219 .get(&(nid, indexed_name.clone()))
220 .unwrap_or(fallback);
221 let _ = writeln!(out, "{}{} = graph", pad, indexed_name);
222 print_graph(out, sub, "", depth, opts);
223 }
224 }
225 _ => {}
226 }
227 }
228}
229
230fn is_subgraph_attr(attr: &Attribute) -> bool {
231 match attr {
232 Attribute::Graph(_) => true,
233 Attribute::Graphs(graphs) => !graphs.is_empty(),
234 _ => false,
235 }
236}
237
238fn typed_value(graph: &Graph, vid: ValueId) -> String {
240 let value = graph.value(vid);
241 let ty = type_string(value.dtype, &value.shape, graph);
242 format!("{} {}", ty, value_name(graph, vid))
243}
244
245fn type_string(dtype: DataType, shape: &Shape, graph: &Graph) -> String {
247 let dims = shape
248 .iter()
249 .map(|d| dim_string(*d, graph))
250 .collect::<Vec<_>>()
251 .join(", ");
252 format!("{}[{}]", dtype_name(dtype), dims)
253}
254
255fn dim_string(dim: Dim, graph: &Graph) -> String {
256 match dim {
257 Dim::Static(n) => n.to_string(),
258 Dim::Symbolic(sid) => graph
259 .symbol_constraints
260 .get(&sid)
261 .and_then(|c| c.name.clone())
262 .map(|name| {
263 if name
264 .chars()
265 .all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
266 {
267 name
268 } else {
269 format!("{name:?}")
270 }
271 })
272 .unwrap_or_else(|| format!("s{}", sid.0)),
273 }
274}
275
276fn value_name(graph: &Graph, vid: ValueId) -> String {
278 match graph.try_value(vid).and_then(|v| v.name.as_deref()) {
279 Some(name) if !name.is_empty() => name.to_string(),
280 _ => format!("%v{}", vid.0),
281 }
282}
283
284fn weight_type(weight: &WeightRef) -> String {
285 let dims = weight
286 .dims()
287 .iter()
288 .map(|d| d.to_string())
289 .collect::<Vec<_>>()
290 .join(", ");
291 format!("{}[{}]", dtype_name(weight.dtype()), dims)
292}
293
294fn weight_kind(weight: &WeightRef) -> &'static str {
295 match weight {
296 WeightRef::Inline(_) => "inline",
297 WeightRef::External { .. } => "external",
298 }
299}
300
301fn attr_value(attr: &Attribute) -> String {
303 match attr {
304 Attribute::Int(v) => v.to_string(),
305 Attribute::Float(v) => format!("{:?}", v),
306 Attribute::String(bytes) => match std::str::from_utf8(bytes) {
307 Ok(s) => format!("{:?}", s),
308 Err(_) => format!("<{} bytes>", bytes.len()),
309 },
310 Attribute::Ints(v) if v.is_empty() => "[]:ints".to_string(),
311 Attribute::Ints(v) => format!(
312 "[{}]",
313 v.iter()
314 .map(|i| i.to_string())
315 .collect::<Vec<_>>()
316 .join(", ")
317 ),
318 Attribute::Floats(v) if v.is_empty() => "[]:floats".to_string(),
319 Attribute::Floats(v) => format!(
320 "[{}]",
321 v.iter()
322 .map(|f| format!("{:?}", f))
323 .collect::<Vec<_>>()
324 .join(", ")
325 ),
326 Attribute::Strings(values) if values.is_empty() => "[]:strings".to_string(),
327 Attribute::Strings(values) => values
328 .iter()
329 .map(|bytes| std::str::from_utf8(bytes).map(|value| format!("{value:?}")))
330 .collect::<Result<Vec<_>, _>>()
331 .map(|values| format!("[{}]", values.join(", ")))
332 .unwrap_or_else(|_| format!("<{} strings>", values.len())),
333 Attribute::Tensor(t) => format!("<tensor {}[{:?}]>", dtype_name(t.dtype), t.dims),
334 Attribute::Tensors(tensors) => format!("<{} tensors>", tensors.len()),
335 Attribute::SparseTensor(_) => "<sparse tensor>".to_string(),
336 Attribute::SparseTensors(tensors) => format!("<{} sparse tensors>", tensors.len()),
337 Attribute::TypeProto(_) => "<type>".to_string(),
338 Attribute::TypeProtos(types) => format!("<{} types>", types.len()),
339 Attribute::Graphs(graphs) if graphs.is_empty() => "[]:graphs".to_string(),
341 Attribute::Graph(_) | Attribute::Graphs(_) => "<graph>".to_string(),
342 }
343}
344
345fn dtype_name(dtype: DataType) -> &'static str {
347 match dtype {
348 DataType::Undefined => "undefined",
349 DataType::Float32 => "float",
350 DataType::Uint8 => "uint8",
351 DataType::Int8 => "int8",
352 DataType::Uint16 => "uint16",
353 DataType::Int16 => "int16",
354 DataType::Int32 => "int32",
355 DataType::Int64 => "int64",
356 DataType::String => "string",
357 DataType::Bool => "bool",
358 DataType::Float16 => "float16",
359 DataType::Float64 => "float64",
360 DataType::Uint32 => "uint32",
361 DataType::Uint64 => "uint64",
362 DataType::Complex64 => "complex64",
363 DataType::Complex128 => "complex128",
364 DataType::BFloat16 => "bfloat16",
365 DataType::Float8E4M3FN => "float8e4m3fn",
366 DataType::Float8E4M3FNUZ => "float8e4m3fnuz",
367 DataType::Float8E5M2 => "float8e5m2",
368 DataType::Float8E5M2FNUZ => "float8e5m2fnuz",
369 DataType::Uint4 => "uint4",
370 DataType::Int4 => "int4",
371 DataType::Float4E2M1 => "float4e2m1",
372 DataType::Float8E8M0 => "float8e8m0",
373 DataType::Uint2 => "uint2",
374 DataType::Int2 => "int2",
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use onnx_runtime_ir::{Node, TensorData, static_shape};
382 use onnx_runtime_loader::ModelMetadata;
383
384 fn add_model() -> Model {
385 let mut g = Graph::new();
386 g.opset_imports.insert(String::new(), 21);
387 let x = g.create_named_value("X", DataType::Float32, static_shape([2, 3]));
388 let y = g.create_named_value("Y", DataType::Float32, static_shape([2, 3]));
389 let z = g.create_named_value("Z", DataType::Float32, static_shape([2, 3]));
390 g.add_input(x);
391 g.add_input(y);
392 let mut node = Node::new(NodeId(0), "Add", vec![Some(x), Some(y)], vec![z]);
393 node.name = "add0".to_string();
394 g.insert_node(node);
395 g.add_output(z);
396 Model::with_metadata(g, ModelMetadata::default())
397 }
398
399 #[test]
400 fn dumps_header_signature_and_node() {
401 let text = to_text(&add_model());
402 assert!(text.contains("ir_version: 10"), "header:\n{text}");
403 assert!(text.contains("opset_import: [\"\" : 21]"), "opset:\n{text}");
404 assert!(
405 text.contains("main (float[2, 3] X, float[2, 3] Y) => (float[2, 3] Z)"),
406 "signature:\n{text}"
407 );
408 assert!(text.contains("Z = Add(X, Y)"), "node:\n{text}");
409 }
410
411 #[test]
412 fn renders_inline_attribute() {
413 let mut g = Graph::new();
414 g.opset_imports.insert(String::new(), 21);
415 let x = g.create_named_value("X", DataType::Float32, static_shape([4]));
416 let y = g.create_named_value("Y", DataType::Float32, static_shape([4]));
417 g.add_input(x);
418 let mut node = Node::new(NodeId(0), "LeakyRelu", vec![Some(x)], vec![y]);
419 node.attributes
420 .insert("alpha".to_string(), Attribute::Float(0.1));
421 g.insert_node(node);
422 g.add_output(y);
423 let text = to_text(&Model::new(g));
424 assert!(text.contains("Y = LeakyRelu <alpha = 0.1>(X)"), "{text}");
425 }
426
427 #[test]
428 fn initializers_are_references_not_data() {
429 let mut g = Graph::new();
430 g.opset_imports.insert(String::new(), 21);
431 let w = g.create_named_value("W", DataType::Float32, static_shape([2]));
432 let init = TensorData::from_raw(DataType::Float32, vec![2], vec![0u8; 8]);
433 g.set_initializer(w, WeightRef::Inline(init));
434 let text = to_text(&Model::new(g));
435 assert!(text.contains("// initializers"), "{text}");
436 assert!(
437 text.contains("float[2] W = <inline data omitted>"),
438 "{text}"
439 );
440 assert!(!text.contains("\\x00"), "{text}");
442 }
443
444 fn unary_subgraph(input: &str, output: &str, op: &str) -> Graph {
445 let mut graph = Graph::new();
446 let x = graph.create_named_value(input, DataType::Float32, static_shape([2]));
447 let y = graph.create_named_value(output, DataType::Float32, static_shape([2]));
448 graph.add_input(x);
449 graph.insert_node(Node::new(NodeId(0), op, vec![Some(x)], vec![y]));
450 graph.add_output(y);
451 graph
452 }
453
454 #[test]
455 fn prints_single_and_list_subgraph_bodies() {
456 let then_branch = unary_subgraph("then_in", "then_out", "Relu");
457 let first_case = unary_subgraph("case0_in", "case0_out", "Neg");
458 let second_case = unary_subgraph("case1_in", "case1_out", "Identity");
459
460 let mut graph = Graph::new();
461 graph.opset_imports.insert(String::new(), 21);
462 let cond = graph.create_named_value("cond", DataType::Bool, static_shape([]));
463 let out = graph.create_named_value("out", DataType::Float32, static_shape([2]));
464 graph.add_input(cond);
465 let mut node = Node::new(NodeId(0), "If", vec![Some(cond)], vec![out]);
466 node.attributes.insert(
467 "then_branch".into(),
468 Attribute::Graph(Box::new(then_branch.clone())),
469 );
470 node.attributes.insert(
471 "branches".into(),
472 Attribute::Graphs(vec![first_case.clone(), second_case.clone()]),
473 );
474 let node_id = graph.insert_node(node);
475 graph
476 .subgraphs
477 .insert((node_id, "then_branch".into()), then_branch);
478 graph
479 .subgraphs
480 .insert((node_id, "branches[0]".into()), first_case);
481 graph
482 .subgraphs
483 .insert((node_id, "branches[1]".into()), second_case);
484 graph.add_output(out);
485
486 let text = to_text(&Model::new(graph));
487 assert!(text.contains("then_branch = graph"), "{text}");
488 assert!(text.contains("then_out = Relu(then_in)"), "{text}");
489 assert!(text.contains("branches[0] = graph"), "{text}");
490 assert!(text.contains("case0_out = Neg(case0_in)"), "{text}");
491 assert!(text.contains("branches[1] = graph"), "{text}");
492 assert!(text.contains("case1_out = Identity(case1_in)"), "{text}");
493 }
494}