Skip to main content

onnx_runtime_ir/
shape.rs

1//! Shapes with static and symbolic (dynamic) dimensions (see `docs/ORT2.md`
2//! §3.2 and §11).
3
4/// Unique identifier for a symbolic dimension.
5///
6/// Two dimensions that share the same protobuf dim-param name (e.g.
7/// `"batch_size"`) are interned to the same `SymbolId` by the loader so that
8/// shape reasoning can equate them.
9#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
10pub struct SymbolId(pub u32);
11
12/// A single dimension: either a concrete size or a symbol.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum Dim {
15    /// A statically known extent.
16    Static(usize),
17    /// A dynamic extent identified by a [`SymbolId`].
18    Symbolic(SymbolId),
19}
20
21impl Dim {
22    /// The concrete size, if statically known.
23    pub fn as_static(self) -> Option<usize> {
24        match self {
25            Dim::Static(n) => Some(n),
26            Dim::Symbolic(_) => None,
27        }
28    }
29
30    /// Whether this dimension is statically known.
31    pub fn is_static(self) -> bool {
32        matches!(self, Dim::Static(_))
33    }
34}
35
36impl From<usize> for Dim {
37    fn from(n: usize) -> Self {
38        Dim::Static(n)
39    }
40}
41
42impl From<SymbolId> for Dim {
43    fn from(s: SymbolId) -> Self {
44        Dim::Symbolic(s)
45    }
46}
47
48/// A tensor shape: an ordered list of [`Dim`]s.
49///
50/// A rank-0 (scalar) shape is the empty vector. The IR `Shape` therefore always
51/// denotes a *known* rank; it does not carry a distinct "unknown rank"
52/// inhabitant (ONNX `tensor_type.shape` entirely absent). The loader maps an
53/// absent `TensorShapeProto` to an empty `Shape`, so unknown-rank values are
54/// currently indistinguishable from rank-0 scalars — an accepted limitation for
55/// the dense fp / quantized models the runtime targets, where value types carry
56/// shapes. Distinguishing the two would require making `Value::shape` optional
57/// across the frozen IR contract and is deliberately out of scope here. Known,
58/// per-axis unknown *dimensions* (of a known rank) are fully modeled via
59/// [`Dim::Symbolic`].
60pub type Shape = Vec<Dim>;
61
62/// Convenience constructor for a fully-static shape.
63pub fn static_shape(dims: impl IntoIterator<Item = usize>) -> Shape {
64    dims.into_iter().map(Dim::Static).collect()
65}
66
67/// Returns `Some(dims)` if every dimension of `shape` is static.
68pub fn as_static_shape(shape: &[Dim]) -> Option<Vec<usize>> {
69    shape.iter().map(|d| d.as_static()).collect()
70}
71
72/// Whether every dimension of `shape` is static.
73pub fn is_fully_static(shape: &[Dim]) -> bool {
74    shape.iter().all(|d| d.is_static())
75}
76
77/// Constraints on a symbolic dimension, used by shape bucketing, kernel
78/// specialization, and tiling (see `docs/ORT2.md` §11).
79#[derive(Clone, Debug, PartialEq, Eq, Default)]
80pub struct SymbolConstraints {
81    pub id: Option<SymbolId>,
82    /// Human-readable name, e.g. `"batch_size"`, `"seq_len"`.
83    pub name: Option<String>,
84    /// Inclusive minimum value.
85    pub min: Option<usize>,
86    /// Inclusive maximum value.
87    pub max: Option<usize>,
88    /// The value must be a multiple of this (for tiling / vectorization).
89    pub divisible_by: Option<usize>,
90}
91
92impl SymbolConstraints {
93    /// A fresh constraint set for `id` with an optional `name`.
94    pub fn new(id: SymbolId, name: Option<String>) -> Self {
95        Self {
96            id: Some(id),
97            name,
98            ..Default::default()
99        }
100    }
101
102    /// Whether `value` satisfies all present constraints.
103    pub fn accepts(&self, value: usize) -> bool {
104        self.min.is_none_or(|lo| value >= lo)
105            && self.max.is_none_or(|hi| value <= hi)
106            && self.divisible_by.is_none_or(|m| m != 0 && value.is_multiple_of(m))
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn static_shape_helpers() {
116        let s = static_shape([2, 3, 4]);
117        assert_eq!(s.len(), 3);
118        assert!(is_fully_static(&s));
119        assert_eq!(as_static_shape(&s), Some(vec![2, 3, 4]));
120    }
121
122    #[test]
123    fn symbolic_shape_is_not_static() {
124        let s = vec![Dim::Symbolic(SymbolId(0)), Dim::Static(768)];
125        assert!(!is_fully_static(&s));
126        assert_eq!(as_static_shape(&s), None);
127        assert_eq!(s[1].as_static(), Some(768));
128    }
129
130    #[test]
131    fn dim_conversions() {
132        assert_eq!(Dim::from(5usize), Dim::Static(5));
133        assert_eq!(Dim::from(SymbolId(7)), Dim::Symbolic(SymbolId(7)));
134    }
135
136    #[test]
137    fn constraints_accept() {
138        let c = SymbolConstraints {
139            id: Some(SymbolId(0)),
140            name: Some("seq".into()),
141            min: Some(1),
142            max: Some(2048),
143            divisible_by: Some(8),
144        };
145        assert!(c.accepts(8));
146        assert!(c.accepts(2048));
147        assert!(!c.accepts(0)); // below min
148        assert!(!c.accepts(4096)); // above max
149        assert!(!c.accepts(12)); // not divisible by 8
150    }
151}