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