Skip to main content

onnx_runtime_shape_inference/
infer.rs

1//! Whole-graph and single-node inference driving logic.
2
3use std::collections::HashMap;
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
14/// Fresh symbolic dimensions minted by this crate live at or above this id, in
15/// the "anonymous" range the loader also reserves (`u32_symbol` starts at
16/// `0x8000_0000`). Graph-interned dim-params (`batch`, `seq_len`, …) stay in the
17/// low range, so a fresh symbol here can never be confused with a named one nor
18/// with a future [`Graph::create_symbol`](onnx_runtime_ir::Graph) allocation
19/// (which advances the graph's private low-range counter).
20const ANON_SYMBOL_FLOOR: u32 = 0x8000_0000;
21
22impl InferenceRegistry {
23    /// Infer shapes for every value in `graph`, in topological order.
24    ///
25    /// Seeds the known types (graph inputs and initializers), runs each node's
26    /// rule to fill its outputs' types and shape-data, then writes the resolved
27    /// shapes back into the graph (lowering symbolic dimension expressions to IR
28    /// [`Dim`]s). Graph outputs are reconciled with their declared shapes under
29    /// `policy`. Returns an [`InferenceReport`] of what resolved.
30    ///
31    /// `opset_imports` selects the effective operator versions; pass
32    /// `graph.opset_imports.clone()` for the model's own imports.
33    pub fn infer_graph(
34        &self,
35        graph: &mut Graph,
36        opset_imports: &HashMap<String, u64>,
37        policy: MergePolicy,
38    ) -> Result<InferenceReport, ShapeInferError> {
39        let order = graph
40            .topological_order()
41            .map_err(|_| ShapeInferError::CycleDetected)?;
42
43        let mut interner = SymbolInterner::new(seed_next_symbol(graph));
44        let mut types: HashMap<ValueId, TypeInfo> = HashMap::new();
45        let mut shape_data: HashMap<ValueId, ShapeData> = HashMap::new();
46
47        seed_sources(graph, &mut types, &mut shape_data);
48
49        // Snapshot graph outputs' declared shapes for the final merge.
50        let declared_out: HashMap<ValueId, Vec<Dim>> = graph
51            .outputs
52            .iter()
53            .filter_map(|&vid| graph.try_value(vid).map(|v| (vid, v.shape.clone())))
54            .collect();
55
56        // Propagate in topological order.
57        for nid in order {
58            let node = graph.node(nid).clone();
59            let inputs = gather_inputs(&node, &types, &shape_data);
60            let outputs = self.infer_node(&node, opset_imports, inputs, policy, &mut interner)?;
61            for (slot, io) in node.outputs.iter().zip(outputs) {
62                if let Some(ti) = io.type_info {
63                    types.insert(*slot, ti);
64                }
65                if let Some(sd) = io.shape_data
66                    && sd.within_bounds()
67                {
68                    shape_data.insert(*slot, sd);
69                }
70            }
71        }
72
73        // Reconcile graph outputs with their declared shapes.
74        for (&vid, declared) in &declared_out {
75            if let Some(ti) = types.get(&vid) {
76                let merged = merge_shapes(vid, &ti.shape, declared, policy)?;
77                let dtype = ti.dtype;
78                types.insert(vid, TypeInfo::new(dtype, merged));
79            }
80        }
81
82        // Write resolved types back into the graph (lowering DimExprs to Dims).
83        let mut resolved = Vec::new();
84        for (&vid, ti) in &types {
85            if graph.try_value(vid).is_none() {
86                continue;
87            }
88            let dims: Vec<Dim> = ti.shape.iter().map(|d| interner.lower(d)).collect();
89            let value = graph.value_mut(vid);
90            value.shape = dims;
91            value.dtype = ti.dtype;
92            resolved.push(vid);
93        }
94
95        // Register any freshly-minted symbols on the graph.
96        for &sym in interner.fresh_symbols() {
97            graph
98                .symbol_constraints
99                .entry(sym)
100                .or_insert_with(|| SymbolConstraints::new(sym, None));
101        }
102
103        let unresolved: Vec<ValueId> = graph
104            .values
105            .keys()
106            .filter(|vid| !types.contains_key(vid))
107            .collect();
108
109        Ok(InferenceReport {
110            total_values: graph.num_values(),
111            fresh_symbols: interner.fresh_symbols().len(),
112            resolved,
113            unresolved,
114        })
115    }
116}
117
118/// Seed the type (and shape-data) of every source value — graph inputs,
119/// initializers, and any other producer-less value.
120fn seed_sources(
121    graph: &Graph,
122    types: &mut HashMap<ValueId, TypeInfo>,
123    shape_data: &mut HashMap<ValueId, ShapeData>,
124) {
125    for (vid, value) in graph.values.iter() {
126        if value.producer.is_some() {
127            continue;
128        }
129        let shape: TypedShape = value.shape.iter().map(|&d| DimExpr::from(d)).collect();
130        types.insert(vid, TypeInfo::new(value.dtype, shape));
131    }
132    // Initializers carry concrete data; capture their shape-data too.
133    for (&vid, weight) in &graph.initializers {
134        if let WeightRef::Inline(t) = weight
135            && let Some(sd) = ShapeData::from_tensor(t.dtype, &t.dims, &t.data)
136        {
137            shape_data.insert(vid, sd);
138        }
139    }
140}
141
142/// Assemble the per-input [`NodeIo`]s for a node, aligned with `node.inputs`.
143fn gather_inputs(
144    node: &Node,
145    types: &HashMap<ValueId, TypeInfo>,
146    shape_data: &HashMap<ValueId, ShapeData>,
147) -> Vec<NodeIo> {
148    node.inputs
149        .iter()
150        .map(|slot| match slot {
151            Some(vid) => NodeIo {
152                type_info: types.get(vid).cloned(),
153                shape_data: shape_data.get(vid).cloned(),
154            },
155            None => NodeIo::default(),
156        })
157        .collect()
158}
159
160/// The first fresh-symbol id to allocate: strictly above every symbol id already
161/// present in the graph, and at least [`ANON_SYMBOL_FLOOR`].
162fn seed_next_symbol(graph: &Graph) -> u32 {
163    let mut max = ANON_SYMBOL_FLOOR.saturating_sub(1);
164    for &SymbolId(id) in graph.symbol_constraints.keys() {
165        max = max.max(id);
166    }
167    for value in graph.values.values() {
168        for dim in &value.shape {
169            if let Dim::Symbolic(SymbolId(id)) = dim {
170                max = max.max(*id);
171            }
172        }
173    }
174    max.saturating_add(1)
175}