Skip to main content

onnx_runtime_shape_inference/
shape_data.rs

1//! Shape-DATA propagation: [`ShapeData`].
2//!
3//! The single most important idea carried over from the reference
4//! implementation ([`justinchuby/onnx-shape-inference`]). Standard shape
5//! inference propagates the *shape* of each tensor; it cannot resolve a
6//! `Reshape` whose target vector is *computed* at runtime from a `Shape` op.
7//! Shape-data propagation additionally tracks the known *element values* of the
8//! small integer tensors that make up those shape-computation subgraphs —
9//! `Shape → Slice → Concat → Gather → Unsqueeze → Reshape` — so the reshape
10//! target resolves symbolically without executing the graph. This is exactly
11//! what lets transformer graphs (BERT and friends) infer statically.
12//!
13//! A [`ShapeData`] models a rank-0 (scalar) or rank-1 (vector) integer tensor
14//! whose elements are [`DimExpr`]s (so they may be concrete *or* symbolic —
15//! e.g. the `batch`/`seq_len` dims read out of a `Shape` op). Only small
16//! integer tensors are tracked; everything else has no shape-data.
17//!
18//! [`justinchuby/onnx-shape-inference`]: https://github.com/justinchuby/onnx-shape-inference
19
20use onnx_runtime_ir::DataType;
21
22use crate::dim_expr::DimExpr;
23/// Upper bound on the element count a [`ShapeData`] will hold. Shape-vectors are
24/// tiny (a handful of dims); this keeps propagation bounded and prevents an
25/// accidental attempt to track a large weight tensor's contents.
26pub const MAX_SHAPE_DATA_ELEMS: usize = 1024;
27
28/// The known element values of a rank-0 or rank-1 integer tensor flowing
29/// through a shape-computation subgraph.
30///
31/// Integer tensors carry their values in [`elems`](Self::elems) as
32/// [`DimExpr`]s. Floating-point *scalar* constants (needed by `Range`, whose
33/// float form the CPU kernel supports) are carried additively in
34/// [`float_elems`](Self::float_elems); for those, `elems` is empty. `Eq` is not
35/// derived because `f64` is not `Eq` — `ShapeData` is only ever used as a map
36/// *value*, so structural equality is not required.
37#[derive(Clone, Debug, PartialEq)]
38pub struct ShapeData {
39    /// The element type (`Int64`/`Int32`, or `Bool` for masks; `Float32`/
40    /// `Float64` for the float-scalar side-channel).
41    pub dtype: DataType,
42    /// Static dimensions. Empty == rank-0 scalar; `[n]` == rank-1 vector.
43    pub dims: Vec<usize>,
44    /// Row-major integer elements. Length is `1` for a scalar, `dims[0]` for a
45    /// vector. Empty for floating-point shape-data (see `float_elems`).
46    pub elems: Vec<DimExpr>,
47    /// Row-major floating-point elements for rank-0/rank-1 floating-point
48    /// constants used by `Range` and `Resize`; `None` otherwise.
49    pub float_elems: Option<Vec<f64>>,
50}
51
52impl ShapeData {
53    /// A rank-0 scalar holding `value`.
54    pub fn scalar(dtype: DataType, value: DimExpr) -> Self {
55        Self {
56            dtype,
57            dims: Vec::new(),
58            elems: vec![value],
59            float_elems: None,
60        }
61    }
62
63    /// A rank-1 vector holding `elems`.
64    pub fn vector(dtype: DataType, elems: Vec<DimExpr>) -> Self {
65        Self {
66            dims: vec![elems.len()],
67            dtype,
68            elems,
69            float_elems: None,
70        }
71    }
72
73    /// A rank-0 floating-point scalar holding `value`.
74    pub fn float_scalar(dtype: DataType, value: f64) -> Self {
75        Self {
76            dtype,
77            dims: Vec::new(),
78            elems: Vec::new(),
79            float_elems: Some(vec![value]),
80        }
81    }
82
83    /// A rank-1 floating-point vector holding `values`.
84    pub fn float_vector(dtype: DataType, values: Vec<f64>) -> Self {
85        Self {
86            dims: vec![values.len()],
87            dtype,
88            elems: Vec::new(),
89            float_elems: Some(values),
90        }
91    }
92
93    /// This value interpreted as a floating-point scalar, if it is one.
94    pub fn as_float_scalar(&self) -> Option<f64> {
95        let floats = self.float_elems.as_ref()?;
96        (self.is_scalar() && floats.len() == 1).then(|| floats[0])
97    }
98
99    /// This value interpreted as a floating-point vector, if it is one.
100    pub fn as_float_vector(&self) -> Option<&[f64]> {
101        (!self.is_scalar()).then_some(self.float_elems.as_deref()?)
102    }
103
104    /// Whether this is a rank-0 scalar.
105    pub fn is_scalar(&self) -> bool {
106        self.dims.is_empty()
107    }
108
109    /// Whether the element count is within [`MAX_SHAPE_DATA_ELEMS`].
110    pub fn within_bounds(&self) -> bool {
111        self.elems.len() <= MAX_SHAPE_DATA_ELEMS
112    }
113
114    /// This value's contents interpreted as a shape (each element is a dim).
115    ///
116    /// Used by `Reshape`/`Expand`/`ConstantOfShape` to turn a resolved shape
117    /// vector into the output shape.
118    pub fn as_shape(&self) -> Vec<DimExpr> {
119        self.elems.clone()
120    }
121
122    /// Extract shape-data from a concrete constant tensor, if it is a small
123    /// (rank ≤ 1, within [`MAX_SHAPE_DATA_ELEMS`]) integer or boolean tensor.
124    ///
125    /// Non-integer tensors, higher-rank tensors, and over-large tensors are not
126    /// tracked (returns `None`) — only the shape-computation operands matter.
127    pub fn from_tensor(dtype: DataType, dims: &[usize], data: &[u8]) -> Option<Self> {
128        if dims.len() > 1 {
129            return None;
130        }
131        let numel: usize = if dims.is_empty() {
132            1
133        } else {
134            dims.iter().product()
135        };
136        if numel > MAX_SHAPE_DATA_ELEMS {
137            return None;
138        }
139        // Small floating-point scalars/vectors are captured for Range/Resize;
140        // higher-rank float tensors (weights) are deliberately not tracked.
141        if dtype.is_float() {
142            let values = read_floats(dtype, numel, data)?;
143            return Some(if dims.is_empty() {
144                Self::float_scalar(dtype, values[0])
145            } else {
146                Self::float_vector(dtype, values)
147            });
148        }
149        let ints = read_ints(dtype, numel, data)?;
150        let elems: Vec<DimExpr> = ints.into_iter().map(DimExpr::constant).collect();
151        Some(Self {
152            dtype,
153            dims: dims.to_vec(),
154            elems,
155            float_elems: None,
156        })
157    }
158}
159
160fn read_floats(dtype: DataType, numel: usize, data: &[u8]) -> Option<Vec<f64>> {
161    match dtype {
162        DataType::Float32 => {
163            if data.len() < numel.checked_mul(4)? {
164                return None;
165            }
166            data.chunks_exact(4)
167                .take(numel)
168                .map(|bytes| Some(f32::from_le_bytes(bytes.try_into().ok()?) as f64))
169                .collect()
170        }
171        DataType::Float64 => {
172            if data.len() < numel.checked_mul(8)? {
173                return None;
174            }
175            data.chunks_exact(8)
176                .take(numel)
177                .map(|bytes| Some(f64::from_le_bytes(bytes.try_into().ok()?)))
178                .collect()
179        }
180        _ => None,
181    }
182}
183
184/// Read `numel` little-endian integer/boolean elements from `data`.
185///
186/// Returns `None` for non-integral dtypes (floating point, string) or a byte
187/// length that does not match `numel` elements.
188fn read_ints(dtype: DataType, numel: usize, data: &[u8]) -> Option<Vec<i64>> {
189    macro_rules! read {
190        ($ty:ty, $sz:expr) => {{
191            if data.len() < numel * $sz {
192                return None;
193            }
194            data.chunks_exact($sz)
195                .take(numel)
196                .map(|c| {
197                    let arr: [u8; $sz] = c.try_into().ok()?;
198                    Some(<$ty>::from_le_bytes(arr) as i64)
199                })
200                .collect::<Option<Vec<i64>>>()
201        }};
202    }
203    match dtype {
204        DataType::Int64 => read!(i64, 8),
205        DataType::Uint64 => read!(u64, 8),
206        DataType::Int32 => read!(i32, 4),
207        DataType::Uint32 => read!(u32, 4),
208        DataType::Int16 => read!(i16, 2),
209        DataType::Uint16 => read!(u16, 2),
210        DataType::Int8 => read!(i8, 1),
211        DataType::Uint8 | DataType::Bool => read!(u8, 1),
212        _ => None,
213    }
214}