Skip to main content

onnx_runtime_shape_inference/
infer.rs

1//! Whole-graph and single-node inference driving logic.
2
3use 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
21/// Fresh symbolic dimensions minted by this crate live at or above this id, in
22/// the "anonymous" range the loader also reserves (`u32_symbol` starts at
23/// `0x8000_0000`). Graph-interned dim-params (`batch`, `seq_len`, …) stay in the
24/// low range, so a fresh symbol here can never be confused with a named one nor
25/// with a future [`Graph::create_symbol`](onnx_runtime_ir::Graph) allocation
26/// (which advances the graph's private low-range counter).
27const ANON_SYMBOL_FLOOR: u32 = 0x8000_0000;
28
29impl InferenceRegistry {
30    /// Infer shapes for every value in `graph`, in topological order.
31    ///
32    /// Seeds every explicitly known value type, runs each node's rule to fill or
33    /// refine its outputs' types and shape-data, then writes the resolved shapes
34    /// back into the graph (lowering symbolic dimension expressions to IR
35    /// [`Dim`]s). Graph outputs are reconciled with their declared shapes under
36    /// `policy`. Returns an [`InferenceReport`] of what resolved.
37    ///
38    /// `opset_imports` selects the effective operator versions; pass
39    /// `graph.opset_imports.clone()` for the model's own imports.
40    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        // Snapshot graph outputs' declared shapes for the final merge.
71        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        // Propagate in topological order.
88        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        // Reconcile graph outputs with their declared shapes.
167        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        // Write resolved types back into the graph (lowering DimExprs to Dims).
176        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        // Register any freshly-minted symbols on the graph.
189        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
326/// Seed every explicitly known value type, including intermediate `value_info`.
327///
328/// A producer rule can overwrite this seed with a freshly inferred type. If the
329/// rule cannot resolve its output, the declared metadata remains available to
330/// downstream consumers instead of being silently discarded.
331fn 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    // Initializers carry concrete data; capture their shape-data too.
344    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
508/// Assemble the per-input [`NodeIo`]s for a node, aligned with `node.inputs`.
509fn 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
526/// The first fresh-symbol id to allocate: strictly above every symbol id already
527/// present in the graph, and at least [`ANON_SYMBOL_FLOOR`].
528fn 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}