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}