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::{
6    DataType, Dim, Graph, Node, SymbolConstraints, SymbolId, ValueId, WeightRef,
7};
8
9use crate::context::{MergePolicy, NodeIo, SymbolInterner, TypeInfo, TypedShape, merge_shapes};
10use crate::dim_expr::DimExpr;
11use crate::error::ShapeInferError;
12use crate::registry::InferenceRegistry;
13use crate::report::InferenceReport;
14use crate::shape_data::ShapeData;
15
16type ScopeBindings = HashMap<String, Option<NodeIo>>;
17
18struct ScopedInference {
19    report: InferenceReport,
20    parent_symbols: HashMap<SymbolId, SymbolId>,
21}
22
23/// Fresh symbolic dimensions minted by this crate live at or above this id, in
24/// the "anonymous" range the loader also reserves (`u32_symbol` starts at
25/// `0x8000_0000`). Graph-interned dim-params (`batch`, `seq_len`, …) stay in the
26/// low range, so a fresh symbol here can never be confused with a named one nor
27/// with a future [`Graph::create_symbol`](onnx_runtime_ir::Graph) allocation
28/// (which advances the graph's private low-range counter).
29const ANON_SYMBOL_FLOOR: u32 = 0x8000_0000;
30
31impl InferenceRegistry {
32    /// Infer shapes for every value in `graph`, in topological order.
33    ///
34    /// Seeds every explicitly known value type, runs each node's rule to fill or
35    /// refine its outputs' types and shape-data, then writes the resolved shapes
36    /// back into the graph (lowering symbolic dimension expressions to IR
37    /// [`Dim`]s). Graph outputs are reconciled with their declared shapes under
38    /// `policy`. Returns an [`InferenceReport`] of what resolved.
39    ///
40    /// `opset_imports` selects the effective operator versions; pass
41    /// `graph.opset_imports.clone()` for the model's own imports.
42    pub fn infer_graph(
43        &self,
44        graph: &mut Graph,
45        opset_imports: &HashMap<String, u64>,
46        policy: MergePolicy,
47    ) -> Result<InferenceReport, ShapeInferError> {
48        self.infer_graph_scoped(graph, opset_imports, policy, &HashMap::new())
49            .map(|result| result.report)
50    }
51
52    fn infer_graph_scoped(
53        &self,
54        graph: &mut Graph,
55        opset_imports: &HashMap<String, u64>,
56        policy: MergePolicy,
57        outer_scope: &ScopeBindings,
58    ) -> Result<ScopedInference, ShapeInferError> {
59        let mut interner = SymbolInterner::new(seed_next_symbol(graph));
60        let (imported_scope, parent_symbols) = import_scope(graph, outer_scope, &mut interner);
61
62        let order = graph
63            .topological_order()
64            .map_err(|_| ShapeInferError::CycleDetected)?;
65
66        let mut types: HashMap<ValueId, TypeInfo> = HashMap::new();
67        let mut shape_data: HashMap<ValueId, ShapeData> = HashMap::new();
68
69        seed_sources(graph, &mut types, &mut shape_data);
70        bind_captures(graph, &imported_scope, &mut types, &mut shape_data);
71
72        // Snapshot graph outputs' declared shapes for the final merge.
73        let declared_out: HashMap<ValueId, Vec<Dim>> = graph
74            .outputs
75            .iter()
76            .filter_map(|&vid| graph.try_value(vid).map(|v| (vid, v.shape.clone())))
77            .collect();
78
79        let mut child_scope = None;
80        let mut scope_sources_added = false;
81        let mut pending_scope_values = Vec::new();
82        let mut remaining_subgraph_nodes = graph
83            .subgraphs
84            .keys()
85            .map(|(owner, _)| *owner)
86            .collect::<HashSet<_>>()
87            .len();
88
89        // Propagate in topological order.
90        for nid in order {
91            let node = graph.node(nid).clone();
92            let mut child_keys: Vec<_> = graph
93                .subgraphs
94                .keys()
95                .filter(|(owner, _)| *owner == nid)
96                .cloned()
97                .collect();
98            child_keys.sort_by(|left, right| left.1.cmp(&right.1));
99            let mut subgraph_results = HashMap::new();
100            if !child_keys.is_empty() {
101                let scope = child_scope.get_or_insert_with(|| imported_scope.clone());
102                if !scope_sources_added {
103                    let formal_inputs: HashSet<_> = graph.inputs.iter().copied().collect();
104                    let source_values: Vec<_> = graph
105                        .values
106                        .iter()
107                        .filter(|(vid, _)| {
108                            formal_inputs.contains(vid) || graph.initializers.contains_key(vid)
109                        })
110                        .map(|(vid, _)| vid)
111                        .collect();
112                    extend_visible_scope(
113                        graph,
114                        &types,
115                        &shape_data,
116                        scope,
117                        source_values,
118                        &mut interner,
119                    );
120                    scope_sources_added = true;
121                }
122                extend_visible_scope(
123                    graph,
124                    &types,
125                    &shape_data,
126                    scope,
127                    pending_scope_values.drain(..),
128                    &mut interner,
129                );
130
131                for key in child_keys {
132                    let subgraph =
133                        graph
134                            .subgraphs
135                            .get_mut(&key)
136                            .ok_or_else(|| ShapeInferError::Invalid {
137                                op: node.op_type.clone(),
138                                detail: format!("subgraph attribute `{}` disappeared", key.1),
139                            })?;
140                    let result = self.infer_graph_scoped(subgraph, opset_imports, policy, scope)?;
141                    subgraph_results.insert(key.1, result);
142                }
143                remaining_subgraph_nodes -= 1;
144            }
145
146            if is_standard_if(&node) {
147                if let Some(outputs) =
148                    infer_if_outputs(graph, &node, &subgraph_results, &mut interner)?
149                {
150                    for (slot, output) in node.outputs.iter().zip(outputs) {
151                        match output {
152                            IfOutput::Typed(type_info) => {
153                                types.insert(*slot, type_info);
154                            }
155                            IfOutput::UnknownRank(element_type) => {
156                                types.remove(slot);
157                                shape_data.remove(slot);
158                                let value = graph.value_mut(*slot);
159                                value.dtype = element_type;
160                                graph.mark_value_type_known(*slot);
161                                graph.mark_value_shape_unknown(*slot);
162                            }
163                            IfOutput::Unresolved => {}
164                        }
165                    }
166                }
167                if remaining_subgraph_nodes > 0 {
168                    pending_scope_values.extend(node.outputs.iter().copied());
169                }
170                continue;
171            }
172
173            let inputs = gather_inputs(&node, &types, &shape_data);
174            let outputs = self.infer_node(&node, opset_imports, inputs, policy, &mut interner)?;
175            for (slot, io) in node.outputs.iter().zip(outputs) {
176                if let Some(ti) = io.type_info {
177                    types.insert(*slot, ti);
178                }
179                if let Some(sd) = io.shape_data
180                    && sd.within_bounds()
181                {
182                    shape_data.insert(*slot, sd);
183                }
184            }
185            if remaining_subgraph_nodes > 0 {
186                pending_scope_values.extend(node.outputs.iter().copied());
187            }
188        }
189
190        // Reconcile graph outputs with their declared shapes.
191        for (&vid, declared) in &declared_out {
192            if let Some(ti) = types.get(&vid) {
193                let merged = merge_shapes(vid, &ti.shape, declared, policy)?;
194                let dtype = ti.dtype;
195                types.insert(vid, TypeInfo::new(dtype, merged));
196            }
197        }
198
199        // Write resolved types back into the graph (lowering DimExprs to Dims).
200        let mut resolved = Vec::new();
201        for (&vid, ti) in &types {
202            if graph.try_value(vid).is_none() {
203                continue;
204            }
205            let dims: Vec<Dim> = ti.shape.iter().map(|d| interner.lower(d)).collect();
206            let value = graph.value_mut(vid);
207            value.shape = dims;
208            value.dtype = ti.dtype;
209            resolved.push(vid);
210        }
211
212        // Register any freshly-minted symbols on the graph.
213        for &sym in interner.fresh_symbols() {
214            graph
215                .symbol_constraints
216                .entry(sym)
217                .or_insert_with(|| SymbolConstraints::new(sym, None));
218        }
219
220        let unresolved: Vec<ValueId> = graph
221            .values
222            .keys()
223            .filter(|vid| !types.contains_key(vid))
224            .collect();
225
226        Ok(ScopedInference {
227            report: InferenceReport {
228                total_values: graph.num_values(),
229                fresh_symbols: interner.fresh_symbols().len(),
230                resolved,
231                unresolved,
232            },
233            parent_symbols,
234        })
235    }
236}
237
238fn is_standard_if(node: &Node) -> bool {
239    node.op_type == "If" && node.is_default_domain()
240}
241
242enum IfOutput {
243    Typed(TypeInfo),
244    UnknownRank(DataType),
245    Unresolved,
246}
247
248fn infer_if_outputs(
249    graph: &Graph,
250    node: &Node,
251    subgraph_results: &HashMap<String, ScopedInference>,
252    interner: &mut SymbolInterner,
253) -> Result<Option<Vec<IfOutput>>, ShapeInferError> {
254    let then_key = (node.id, "then_branch".to_string());
255    let else_key = (node.id, "else_branch".to_string());
256    let Some(then_branch) = graph.subgraphs.get(&then_key) else {
257        return Ok(None);
258    };
259    let Some(else_branch) = graph.subgraphs.get(&else_key) else {
260        return Ok(None);
261    };
262    let Some(then_result) = subgraph_results.get("then_branch") else {
263        return Ok(None);
264    };
265    let Some(else_result) = subgraph_results.get("else_branch") else {
266        return Ok(None);
267    };
268    let then_resolved: HashSet<_> = then_result.report.resolved.iter().copied().collect();
269    let else_resolved: HashSet<_> = else_result.report.resolved.iter().copied().collect();
270
271    let paired_outputs = node
272        .outputs
273        .len()
274        .min(then_branch.outputs.len())
275        .min(else_branch.outputs.len());
276    let mut outputs = Vec::with_capacity(node.outputs.len());
277    for (&then_id, &else_id) in then_branch
278        .outputs
279        .iter()
280        .zip(&else_branch.outputs)
281        .take(paired_outputs)
282    {
283        if !branch_output_is_resolved(then_branch, then_id, &then_resolved)
284            || !branch_output_is_resolved(else_branch, else_id, &else_resolved)
285        {
286            outputs.push(IfOutput::Unresolved);
287            continue;
288        }
289        let then_value =
290            then_branch
291                .try_value(then_id)
292                .ok_or_else(|| ShapeInferError::Invalid {
293                    op: "If".to_string(),
294                    detail: format!("then_branch output {then_id:?} is not live"),
295                })?;
296        let else_value =
297            else_branch
298                .try_value(else_id)
299                .ok_or_else(|| ShapeInferError::Invalid {
300                    op: "If".to_string(),
301                    detail: format!("else_branch output {else_id:?} is not live"),
302                })?;
303
304        if then_value.dtype != else_value.dtype {
305            return Err(ShapeInferError::Invalid {
306                op: "If".to_string(),
307                detail: format!(
308                    "branch output element types differ: {:?} != {:?}",
309                    then_value.dtype, else_value.dtype
310                ),
311            });
312        }
313
314        if then_value.shape.len() != else_value.shape.len() {
315            outputs.push(IfOutput::UnknownRank(then_value.dtype));
316            continue;
317        }
318
319        let shape = then_value
320            .shape
321            .iter()
322            .zip(&else_value.shape)
323            .map(|(&then_dim, &else_dim)| match (then_dim, else_dim) {
324                (Dim::Static(then_size), Dim::Static(else_size)) if then_size == else_size => {
325                    i64::try_from(then_size)
326                        .map(DimExpr::constant)
327                        .unwrap_or_else(|_| interner.fresh_dim())
328                }
329                (Dim::Symbolic(then_symbol), Dim::Symbolic(else_symbol))
330                    if then_result.parent_symbols.get(&then_symbol)
331                        == else_result.parent_symbols.get(&else_symbol)
332                        && then_result.parent_symbols.contains_key(&then_symbol) =>
333                {
334                    DimExpr::symbol(then_result.parent_symbols[&then_symbol])
335                }
336                _ => interner.fresh_dim(),
337            })
338            .collect();
339        outputs.push(IfOutput::Typed(TypeInfo::new(then_value.dtype, shape)));
340    }
341    outputs.resize_with(node.outputs.len(), || IfOutput::Unresolved);
342
343    Ok(Some(outputs))
344}
345
346fn branch_output_is_resolved(branch: &Graph, output: ValueId, resolved: &HashSet<ValueId>) -> bool {
347    resolved.contains(&output) && branch.try_value(output).is_some()
348}
349
350/// Seed every explicitly known value type, including intermediate `value_info`.
351///
352/// A producer rule can overwrite this seed with a freshly inferred type. If the
353/// rule cannot resolve its output, the declared metadata remains available to
354/// downstream consumers instead of being silently discarded.
355fn seed_sources(
356    graph: &Graph,
357    types: &mut HashMap<ValueId, TypeInfo>,
358    shape_data: &mut HashMap<ValueId, ShapeData>,
359) {
360    for (vid, value) in graph.values.iter() {
361        if !graph.value_type_is_known(vid) || !graph.value_shape_is_known(vid) {
362            continue;
363        }
364        let shape: TypedShape = value.shape.iter().map(|&d| DimExpr::from(d)).collect();
365        types.insert(vid, TypeInfo::new(value.dtype, shape));
366    }
367    // Initializers carry concrete data; capture their shape-data too.
368    for (&vid, weight) in &graph.initializers {
369        if let WeightRef::Inline(t) = weight
370            && let Some(sd) = ShapeData::from_tensor(t.dtype, &t.dims, &t.data)
371        {
372            shape_data.insert(vid, sd);
373        }
374    }
375}
376
377fn bind_captures(
378    graph: &Graph,
379    scope: &ScopeBindings,
380    types: &mut HashMap<ValueId, TypeInfo>,
381    shape_data: &mut HashMap<ValueId, ShapeData>,
382) {
383    let formal_inputs: HashSet<_> = graph.inputs.iter().copied().collect();
384    for (vid, value) in graph.values.iter() {
385        if value.producer.is_some()
386            || formal_inputs.contains(&vid)
387            || graph.initializers.contains_key(&vid)
388        {
389            continue;
390        }
391        let Some(name) = value.name.as_deref() else {
392            continue;
393        };
394        let Some(Some(binding)) = scope.get(name) else {
395            continue;
396        };
397        if let Some(type_info) = &binding.type_info {
398            types.insert(vid, type_info.clone());
399        }
400        if let Some(data) = &binding.shape_data {
401            shape_data.insert(vid, data.clone());
402        }
403    }
404}
405
406fn import_scope(
407    graph: &Graph,
408    outer_scope: &ScopeBindings,
409    interner: &mut SymbolInterner,
410) -> (ScopeBindings, HashMap<SymbolId, SymbolId>) {
411    let local_names = local_value_names(graph);
412    let mut parent_to_child = HashMap::new();
413    let mut child_to_parent = HashMap::new();
414    let mut names: Vec<_> = outer_scope
415        .keys()
416        .filter(|name| !local_names.contains(name.as_str()))
417        .collect();
418    names.sort_unstable();
419    let imported = names
420        .into_iter()
421        .map(|name| {
422            let binding = outer_scope[name]
423                .as_ref()
424                .map(|io| remap_node_io(io, interner, &mut parent_to_child, &mut child_to_parent));
425            (name.clone(), binding)
426        })
427        .collect();
428    (imported, child_to_parent)
429}
430
431fn local_value_names(graph: &Graph) -> HashSet<&str> {
432    let formal_inputs: HashSet<_> = graph.inputs.iter().copied().collect();
433    graph
434        .values
435        .iter()
436        .filter(|(vid, value)| {
437            value.producer.is_some()
438                || formal_inputs.contains(vid)
439                || graph.initializers.contains_key(vid)
440        })
441        .filter_map(|(_, value)| value.name.as_deref())
442        .collect()
443}
444
445fn remap_node_io(
446    io: &NodeIo,
447    interner: &mut SymbolInterner,
448    parent_to_child: &mut HashMap<SymbolId, SymbolId>,
449    child_to_parent: &mut HashMap<SymbolId, SymbolId>,
450) -> NodeIo {
451    NodeIo {
452        type_info: io.type_info.as_ref().map(|type_info| {
453            TypeInfo::new(
454                type_info.dtype,
455                type_info
456                    .shape
457                    .iter()
458                    .map(|dim| remap_dim_expr(dim, interner, parent_to_child, child_to_parent))
459                    .collect(),
460            )
461        }),
462        shape_data: io.shape_data.as_ref().map(|data| {
463            let mut data = data.clone();
464            data.elems = data
465                .elems
466                .iter()
467                .map(|dim| remap_dim_expr(dim, interner, parent_to_child, child_to_parent))
468                .collect();
469            data
470        }),
471    }
472}
473
474fn remap_dim_expr(
475    dim: &DimExpr,
476    interner: &mut SymbolInterner,
477    parent_to_child: &mut HashMap<SymbolId, SymbolId>,
478    child_to_parent: &mut HashMap<SymbolId, SymbolId>,
479) -> DimExpr {
480    if let Some(value) = dim.as_const() {
481        return DimExpr::constant(value);
482    }
483    let Some(parent) = dim.as_symbol() else {
484        return interner.fresh_dim();
485    };
486    let child = *parent_to_child.entry(parent).or_insert_with(|| {
487        let child = interner.fresh_symbol();
488        child_to_parent.insert(child, parent);
489        child
490    });
491    DimExpr::symbol(child)
492}
493
494fn extend_visible_scope(
495    graph: &Graph,
496    types: &HashMap<ValueId, TypeInfo>,
497    shape_data: &HashMap<ValueId, ShapeData>,
498    scope: &mut ScopeBindings,
499    values: impl IntoIterator<Item = ValueId>,
500    interner: &mut SymbolInterner,
501) {
502    for vid in values {
503        let Some(value) = graph.try_value(vid) else {
504            continue;
505        };
506        let Some(name) = value.name.as_ref() else {
507            continue;
508        };
509        let binding = types.get(&vid).map(|type_info| NodeIo {
510            type_info: Some(TypeInfo::new(
511                type_info.dtype,
512                type_info
513                    .shape
514                    .iter()
515                    .map(|dim| DimExpr::from(interner.lower(dim)))
516                    .collect(),
517            )),
518            shape_data: shape_data.get(&vid).map(|data| {
519                let mut data = data.clone();
520                data.elems = data
521                    .elems
522                    .iter()
523                    .map(|dim| DimExpr::from(interner.lower(dim)))
524                    .collect();
525                data
526            }),
527        });
528        scope.insert(name.clone(), binding);
529    }
530}
531
532/// Assemble the per-input [`NodeIo`]s for a node, aligned with `node.inputs`.
533fn gather_inputs(
534    node: &Node,
535    types: &HashMap<ValueId, TypeInfo>,
536    shape_data: &HashMap<ValueId, ShapeData>,
537) -> Vec<NodeIo> {
538    node.inputs
539        .iter()
540        .map(|slot| match slot {
541            Some(vid) => NodeIo {
542                type_info: types.get(vid).cloned(),
543                shape_data: shape_data.get(vid).cloned(),
544            },
545            None => NodeIo::default(),
546        })
547        .collect()
548}
549
550/// The first fresh-symbol id to allocate: strictly above every symbol id already
551/// present in the graph, and at least [`ANON_SYMBOL_FLOOR`].
552fn seed_next_symbol(graph: &Graph) -> u32 {
553    let mut max = ANON_SYMBOL_FLOOR.saturating_sub(1);
554    for &SymbolId(id) in graph.symbol_constraints.keys() {
555        max = max.max(id);
556    }
557    for value in graph.values.values() {
558        for dim in &value.shape {
559            if let Dim::Symbolic(SymbolId(id)) = dim {
560                max = max.max(*id);
561            }
562        }
563    }
564    max.saturating_add(1)
565}