1use std::collections::{HashMap, HashSet};
4
5use onnx_runtime_ir::{Dim, Graph, Node, SymbolConstraints, SymbolId, ValueId, WeightRef};
6
7use crate::context::{MergePolicy, NodeIo, SymbolInterner, TypeInfo, TypedShape, merge_shapes};
8use crate::dim_expr::DimExpr;
9use crate::error::ShapeInferError;
10use crate::registry::InferenceRegistry;
11use crate::report::InferenceReport;
12use crate::shape_data::ShapeData;
13
14type ScopeBindings = HashMap<String, Option<NodeIo>>;
15
16struct ScopedInference {
17 report: InferenceReport,
18 parent_symbols: HashMap<SymbolId, SymbolId>,
19}
20
21const ANON_SYMBOL_FLOOR: u32 = 0x8000_0000;
28
29impl InferenceRegistry {
30 pub fn infer_graph(
41 &self,
42 graph: &mut Graph,
43 opset_imports: &HashMap<String, u64>,
44 policy: MergePolicy,
45 ) -> Result<InferenceReport, ShapeInferError> {
46 self.infer_graph_scoped(graph, opset_imports, policy, &HashMap::new())
47 .map(|result| result.report)
48 }
49
50 fn infer_graph_scoped(
51 &self,
52 graph: &mut Graph,
53 opset_imports: &HashMap<String, u64>,
54 policy: MergePolicy,
55 outer_scope: &ScopeBindings,
56 ) -> Result<ScopedInference, ShapeInferError> {
57 let mut interner = SymbolInterner::new(seed_next_symbol(graph));
58 let (imported_scope, parent_symbols) = import_scope(graph, outer_scope, &mut interner);
59
60 let order = graph
61 .topological_order()
62 .map_err(|_| ShapeInferError::CycleDetected)?;
63
64 let mut types: HashMap<ValueId, TypeInfo> = HashMap::new();
65 let mut shape_data: HashMap<ValueId, ShapeData> = HashMap::new();
66
67 seed_sources(graph, &mut types, &mut shape_data);
68 bind_captures(graph, &imported_scope, &mut types, &mut shape_data);
69
70 let declared_out: HashMap<ValueId, Vec<Dim>> = graph
72 .outputs
73 .iter()
74 .filter_map(|&vid| graph.try_value(vid).map(|v| (vid, v.shape.clone())))
75 .collect();
76
77 let mut child_scope = None;
78 let mut scope_sources_added = false;
79 let mut pending_scope_values = Vec::new();
80 let mut remaining_subgraph_nodes = graph
81 .subgraphs
82 .keys()
83 .map(|(owner, _)| *owner)
84 .collect::<HashSet<_>>()
85 .len();
86
87 for nid in order {
89 let node = graph.node(nid).clone();
90 let mut child_keys: Vec<_> = graph
91 .subgraphs
92 .keys()
93 .filter(|(owner, _)| *owner == nid)
94 .cloned()
95 .collect();
96 child_keys.sort_by(|left, right| left.1.cmp(&right.1));
97 let mut subgraph_results = HashMap::new();
98 if !child_keys.is_empty() {
99 let scope = child_scope.get_or_insert_with(|| imported_scope.clone());
100 if !scope_sources_added {
101 let formal_inputs: HashSet<_> = graph.inputs.iter().copied().collect();
102 let source_values: Vec<_> = graph
103 .values
104 .iter()
105 .filter(|(vid, _)| {
106 formal_inputs.contains(vid) || graph.initializers.contains_key(vid)
107 })
108 .map(|(vid, _)| vid)
109 .collect();
110 extend_visible_scope(
111 graph,
112 &types,
113 &shape_data,
114 scope,
115 source_values,
116 &mut interner,
117 );
118 scope_sources_added = true;
119 }
120 extend_visible_scope(
121 graph,
122 &types,
123 &shape_data,
124 scope,
125 pending_scope_values.drain(..),
126 &mut interner,
127 );
128
129 for key in child_keys {
130 let subgraph =
131 graph
132 .subgraphs
133 .get_mut(&key)
134 .ok_or_else(|| ShapeInferError::Invalid {
135 op: node.op_type.clone(),
136 detail: format!("subgraph attribute `{}` disappeared", key.1),
137 })?;
138 let result = self.infer_graph_scoped(subgraph, opset_imports, policy, scope)?;
139 subgraph_results.insert(key.1, result);
140 }
141 remaining_subgraph_nodes -= 1;
142 }
143
144 let inputs = gather_inputs(&node, &types, &shape_data);
145 let outputs = if is_standard_if(&node) {
146 infer_if_outputs(graph, &node, &subgraph_results, &mut interner)?
147 .unwrap_or_else(|| vec![NodeIo::default(); node.outputs.len()])
148 } else {
149 self.infer_node(&node, opset_imports, inputs, policy, &mut interner)?
150 };
151 for (slot, io) in node.outputs.iter().zip(outputs) {
152 if let Some(ti) = io.type_info {
153 types.insert(*slot, ti);
154 }
155 if let Some(sd) = io.shape_data
156 && sd.within_bounds()
157 {
158 shape_data.insert(*slot, sd);
159 }
160 }
161 if remaining_subgraph_nodes > 0 {
162 pending_scope_values.extend(node.outputs.iter().copied());
163 }
164 }
165
166 for (&vid, declared) in &declared_out {
168 if let Some(ti) = types.get(&vid) {
169 let merged = merge_shapes(vid, &ti.shape, declared, policy)?;
170 let dtype = ti.dtype;
171 types.insert(vid, TypeInfo::new(dtype, merged));
172 }
173 }
174
175 let mut resolved = Vec::new();
177 for (&vid, ti) in &types {
178 if graph.try_value(vid).is_none() {
179 continue;
180 }
181 let dims: Vec<Dim> = ti.shape.iter().map(|d| interner.lower(d)).collect();
182 let value = graph.value_mut(vid);
183 value.shape = dims;
184 value.dtype = ti.dtype;
185 resolved.push(vid);
186 }
187
188 for &sym in interner.fresh_symbols() {
190 graph
191 .symbol_constraints
192 .entry(sym)
193 .or_insert_with(|| SymbolConstraints::new(sym, None));
194 }
195
196 let unresolved: Vec<ValueId> = graph
197 .values
198 .keys()
199 .filter(|vid| !types.contains_key(vid))
200 .collect();
201
202 Ok(ScopedInference {
203 report: InferenceReport {
204 total_values: graph.num_values(),
205 fresh_symbols: interner.fresh_symbols().len(),
206 resolved,
207 unresolved,
208 },
209 parent_symbols,
210 })
211 }
212}
213
214fn is_standard_if(node: &Node) -> bool {
215 node.op_type == "If" && (node.domain.is_empty() || node.domain == "ai.onnx")
216}
217
218fn infer_if_outputs(
219 graph: &Graph,
220 node: &Node,
221 subgraph_results: &HashMap<String, ScopedInference>,
222 interner: &mut SymbolInterner,
223) -> Result<Option<Vec<NodeIo>>, ShapeInferError> {
224 let then_key = (node.id, "then_branch".to_string());
225 let else_key = (node.id, "else_branch".to_string());
226 let Some(then_branch) = graph.subgraphs.get(&then_key) else {
227 return Ok(None);
228 };
229 let Some(else_branch) = graph.subgraphs.get(&else_key) else {
230 return Ok(None);
231 };
232 let Some(then_result) = subgraph_results.get("then_branch") else {
233 return Ok(None);
234 };
235 let Some(else_result) = subgraph_results.get("else_branch") else {
236 return Ok(None);
237 };
238 let then_resolved: HashSet<_> = then_result.report.resolved.iter().copied().collect();
239 let else_resolved: HashSet<_> = else_result.report.resolved.iter().copied().collect();
240
241 let paired_outputs = node
242 .outputs
243 .len()
244 .min(then_branch.outputs.len())
245 .min(else_branch.outputs.len());
246 let mut outputs = Vec::with_capacity(node.outputs.len());
247 for (&then_id, &else_id) in then_branch
248 .outputs
249 .iter()
250 .zip(&else_branch.outputs)
251 .take(paired_outputs)
252 {
253 if !branch_output_is_resolved(then_branch, then_id, &then_resolved)
254 || !branch_output_is_resolved(else_branch, else_id, &else_resolved)
255 {
256 outputs.push(NodeIo::default());
257 continue;
258 }
259 let then_value =
260 then_branch
261 .try_value(then_id)
262 .ok_or_else(|| ShapeInferError::Invalid {
263 op: "If".to_string(),
264 detail: format!("then_branch output {then_id:?} is not live"),
265 })?;
266 let else_value =
267 else_branch
268 .try_value(else_id)
269 .ok_or_else(|| ShapeInferError::Invalid {
270 op: "If".to_string(),
271 detail: format!("else_branch output {else_id:?} is not live"),
272 })?;
273
274 if then_value.dtype != else_value.dtype {
275 return Err(ShapeInferError::Invalid {
276 op: "If".to_string(),
277 detail: format!(
278 "branch output element types differ: {:?} != {:?}",
279 then_value.dtype, else_value.dtype
280 ),
281 });
282 }
283
284 if then_value.shape.len() != else_value.shape.len() {
285 return Err(ShapeInferError::Invalid {
286 op: "If".to_string(),
287 detail: format!(
288 "branch output ranks differ: {} != {}",
289 then_value.shape.len(),
290 else_value.shape.len()
291 ),
292 });
293 }
294
295 let shape = then_value
296 .shape
297 .iter()
298 .zip(&else_value.shape)
299 .map(|(&then_dim, &else_dim)| match (then_dim, else_dim) {
300 (Dim::Static(then_size), Dim::Static(else_size)) if then_size == else_size => {
301 i64::try_from(then_size)
302 .map(DimExpr::constant)
303 .unwrap_or_else(|_| interner.fresh_dim())
304 }
305 (Dim::Symbolic(then_symbol), Dim::Symbolic(else_symbol))
306 if then_result.parent_symbols.get(&then_symbol)
307 == else_result.parent_symbols.get(&else_symbol)
308 && then_result.parent_symbols.contains_key(&then_symbol) =>
309 {
310 DimExpr::symbol(then_result.parent_symbols[&then_symbol])
311 }
312 _ => interner.fresh_dim(),
313 })
314 .collect();
315 outputs.push(NodeIo::typed(TypeInfo::new(then_value.dtype, shape)));
316 }
317 outputs.resize_with(node.outputs.len(), NodeIo::default);
318
319 Ok(Some(outputs))
320}
321
322fn branch_output_is_resolved(branch: &Graph, output: ValueId, resolved: &HashSet<ValueId>) -> bool {
323 resolved.contains(&output) && branch.try_value(output).is_some()
324}
325
326fn seed_sources(
332 graph: &Graph,
333 types: &mut HashMap<ValueId, TypeInfo>,
334 shape_data: &mut HashMap<ValueId, ShapeData>,
335) {
336 for (vid, value) in graph.values.iter() {
337 if !graph.value_type_is_known(vid) || !graph.value_shape_is_known(vid) {
338 continue;
339 }
340 let shape: TypedShape = value.shape.iter().map(|&d| DimExpr::from(d)).collect();
341 types.insert(vid, TypeInfo::new(value.dtype, shape));
342 }
343 for (&vid, weight) in &graph.initializers {
345 if let WeightRef::Inline(t) = weight
346 && let Some(sd) = ShapeData::from_tensor(t.dtype, &t.dims, &t.data)
347 {
348 shape_data.insert(vid, sd);
349 }
350 }
351}
352
353fn bind_captures(
354 graph: &Graph,
355 scope: &ScopeBindings,
356 types: &mut HashMap<ValueId, TypeInfo>,
357 shape_data: &mut HashMap<ValueId, ShapeData>,
358) {
359 let formal_inputs: HashSet<_> = graph.inputs.iter().copied().collect();
360 for (vid, value) in graph.values.iter() {
361 if value.producer.is_some()
362 || formal_inputs.contains(&vid)
363 || graph.initializers.contains_key(&vid)
364 {
365 continue;
366 }
367 let Some(name) = value.name.as_deref() else {
368 continue;
369 };
370 let Some(Some(binding)) = scope.get(name) else {
371 continue;
372 };
373 if let Some(type_info) = &binding.type_info {
374 types.insert(vid, type_info.clone());
375 }
376 if let Some(data) = &binding.shape_data {
377 shape_data.insert(vid, data.clone());
378 }
379 }
380}
381
382fn import_scope(
383 graph: &Graph,
384 outer_scope: &ScopeBindings,
385 interner: &mut SymbolInterner,
386) -> (ScopeBindings, HashMap<SymbolId, SymbolId>) {
387 let local_names = local_value_names(graph);
388 let mut parent_to_child = HashMap::new();
389 let mut child_to_parent = HashMap::new();
390 let mut names: Vec<_> = outer_scope
391 .keys()
392 .filter(|name| !local_names.contains(name.as_str()))
393 .collect();
394 names.sort_unstable();
395 let imported = names
396 .into_iter()
397 .map(|name| {
398 let binding = outer_scope[name]
399 .as_ref()
400 .map(|io| remap_node_io(io, interner, &mut parent_to_child, &mut child_to_parent));
401 (name.clone(), binding)
402 })
403 .collect();
404 (imported, child_to_parent)
405}
406
407fn local_value_names(graph: &Graph) -> HashSet<&str> {
408 let formal_inputs: HashSet<_> = graph.inputs.iter().copied().collect();
409 graph
410 .values
411 .iter()
412 .filter(|(vid, value)| {
413 value.producer.is_some()
414 || formal_inputs.contains(vid)
415 || graph.initializers.contains_key(vid)
416 })
417 .filter_map(|(_, value)| value.name.as_deref())
418 .collect()
419}
420
421fn remap_node_io(
422 io: &NodeIo,
423 interner: &mut SymbolInterner,
424 parent_to_child: &mut HashMap<SymbolId, SymbolId>,
425 child_to_parent: &mut HashMap<SymbolId, SymbolId>,
426) -> NodeIo {
427 NodeIo {
428 type_info: io.type_info.as_ref().map(|type_info| {
429 TypeInfo::new(
430 type_info.dtype,
431 type_info
432 .shape
433 .iter()
434 .map(|dim| remap_dim_expr(dim, interner, parent_to_child, child_to_parent))
435 .collect(),
436 )
437 }),
438 shape_data: io.shape_data.as_ref().map(|data| {
439 let mut data = data.clone();
440 data.elems = data
441 .elems
442 .iter()
443 .map(|dim| remap_dim_expr(dim, interner, parent_to_child, child_to_parent))
444 .collect();
445 data
446 }),
447 }
448}
449
450fn remap_dim_expr(
451 dim: &DimExpr,
452 interner: &mut SymbolInterner,
453 parent_to_child: &mut HashMap<SymbolId, SymbolId>,
454 child_to_parent: &mut HashMap<SymbolId, SymbolId>,
455) -> DimExpr {
456 if let Some(value) = dim.as_const() {
457 return DimExpr::constant(value);
458 }
459 let Some(parent) = dim.as_symbol() else {
460 return interner.fresh_dim();
461 };
462 let child = *parent_to_child.entry(parent).or_insert_with(|| {
463 let child = interner.fresh_symbol();
464 child_to_parent.insert(child, parent);
465 child
466 });
467 DimExpr::symbol(child)
468}
469
470fn extend_visible_scope(
471 graph: &Graph,
472 types: &HashMap<ValueId, TypeInfo>,
473 shape_data: &HashMap<ValueId, ShapeData>,
474 scope: &mut ScopeBindings,
475 values: impl IntoIterator<Item = ValueId>,
476 interner: &mut SymbolInterner,
477) {
478 for vid in values {
479 let Some(value) = graph.try_value(vid) else {
480 continue;
481 };
482 let Some(name) = value.name.as_ref() else {
483 continue;
484 };
485 let binding = types.get(&vid).map(|type_info| NodeIo {
486 type_info: Some(TypeInfo::new(
487 type_info.dtype,
488 type_info
489 .shape
490 .iter()
491 .map(|dim| DimExpr::from(interner.lower(dim)))
492 .collect(),
493 )),
494 shape_data: shape_data.get(&vid).map(|data| {
495 let mut data = data.clone();
496 data.elems = data
497 .elems
498 .iter()
499 .map(|dim| DimExpr::from(interner.lower(dim)))
500 .collect();
501 data
502 }),
503 });
504 scope.insert(name.clone(), binding);
505 }
506}
507
508fn gather_inputs(
510 node: &Node,
511 types: &HashMap<ValueId, TypeInfo>,
512 shape_data: &HashMap<ValueId, ShapeData>,
513) -> Vec<NodeIo> {
514 node.inputs
515 .iter()
516 .map(|slot| match slot {
517 Some(vid) => NodeIo {
518 type_info: types.get(vid).cloned(),
519 shape_data: shape_data.get(vid).cloned(),
520 },
521 None => NodeIo::default(),
522 })
523 .collect()
524}
525
526fn seed_next_symbol(graph: &Graph) -> u32 {
529 let mut max = ANON_SYMBOL_FLOOR.saturating_sub(1);
530 for &SymbolId(id) in graph.symbol_constraints.keys() {
531 max = max.max(id);
532 }
533 for value in graph.values.values() {
534 for dim in &value.shape {
535 if let Dim::Symbolic(SymbolId(id)) = dim {
536 max = max.max(*id);
537 }
538 }
539 }
540 max.saturating_add(1)
541}