Skip to main content

tract_data/dim/
mod.rs

1//! Extended dimension support
2use crate::internal::*;
3use num_traits::{One, Zero};
4use std::fmt;
5use std::ops;
6
7mod assertion;
8mod parse;
9mod resolve;
10mod sym;
11mod tree;
12
13pub use self::assertion::Assertion;
14pub use self::parse::parse_tdim;
15pub use self::resolve::solve_for;
16pub use self::sym::{Symbol, SymbolScope, SymbolValues};
17pub use self::tree::{TDim, TooEarly};
18
19use crate::{TractError, TractResult};
20
21/// A super-trait for value acting as tensor dimensions in tract.
22///
23/// Implemented by:
24///
25/// * `usize` for regular dimensions
26/// * `TDim` supporting regular and streaming dimensions
27pub trait DimLike:
28    Clone
29    + Default
30    + PartialEq
31    + From<usize>
32    + for<'a> std::convert::TryFrom<&'a TDim, Error = TractError>
33    + ::num_traits::Zero
34    + fmt::Debug
35    + fmt::Display
36    + std::hash::Hash
37    + ops::Add<Self, Output = Self>
38    + ops::Add<usize, Output = Self>
39    + for<'a> ops::Add<&'a Self, Output = Self>
40    + ops::Sub<Self, Output = Self>
41    + ops::Sub<usize, Output = Self>
42    + for<'a> ops::Sub<&'a Self, Output = Self>
43    + ops::Mul<Self, Output = Self>
44    + ops::Mul<usize, Output = Self>
45    + for<'a> ops::Mul<&'a Self, Output = Self>
46    + ops::Div<usize, Output = Self>
47    + ops::Rem<usize, Output = Self>
48    + Send
49    + Sync
50    + 'static
51    + std::iter::Sum
52    + std::iter::Product
53    + ToDim
54    + One
55{
56    fn maybe_div(&self, other: &Self) -> TractResult<(Self, u64)>;
57
58    /// Integer divise, rounding up to next integer.
59    fn divceil(&self, other: usize) -> Self {
60        (self.clone() + other - 1) / other
61    }
62
63    /// Convert to regular integer, or a lightweight `TooEarly` for a symbolic
64    /// dim. The error carries no anyhow conversion or backtrace, so discarding
65    /// it to probe concreteness is cheap; `?` in a `TractResult` context still
66    /// promotes it to a full error.
67    fn to_i64(&self) -> Result<i64, TooEarly>;
68
69    /// Non-erroring counterpart of `to_i64`: `Some` iff the dim is a plain
70    /// constant, `None` for a symbolic or undetermined dim.
71    fn as_i64(&self) -> Option<i64>;
72
73    fn to_usize(&self) -> Result<usize, TooEarly> {
74        self.to_i64().map(|d| d as usize)
75    }
76
77    fn as_usize(&self) -> Option<usize> {
78        self.as_i64().map(|d| d as usize)
79    }
80
81    fn to_isize(&self) -> Result<isize, TooEarly> {
82        self.to_i64().map(|d| d as isize)
83    }
84
85    fn as_isize(&self) -> Option<isize> {
86        self.as_i64().map(|d| d as isize)
87    }
88
89    fn to_i32(&self) -> Result<i32, TooEarly> {
90        self.to_i64().map(|d| d as i32)
91    }
92
93    /// Substitute as many symbols as possible in the dim value.
94    fn eval(&self, values: &SymbolValues) -> Self;
95
96    /// Full evaluation of the symbol, failing if a symbol is missing
97    fn eval_to_i64(&self, values: &SymbolValues) -> TractResult<i64>;
98
99    fn substitute(&self, from: &Symbol, to: &Self) -> TractResult<Self>;
100    fn substitute_all(&self, map: &std::collections::HashMap<Symbol, Self>) -> TractResult<Self>;
101
102    fn broadcast(self, other: Self) -> TractResult<Self>;
103    fn mini(self, other: Self) -> Self;
104    fn maxi(self, other: Self) -> Self;
105
106    fn compatible_with(&self, other: &Self) -> bool;
107}
108
109impl DimLike for TDim {
110    fn maybe_div(&self, other: &Self) -> TractResult<(Self, u64)> {
111        if self.is_zero() {
112            return Ok((TDim::zero(), 1));
113        } else if other.is_zero() {
114            bail!("Division by zero")
115        }
116        // If self is a sum and every term divides by `other` with the same
117        // remaining divisor, distribute: (a + b) / d = a/d + b/d. This
118        // catches shared symbolic factors the bag-based path below misses,
119        // e.g. `8*slice*((T+7)/8) + 8*((T+7)/8)` divided by `8*((T+7)/8)`.
120        if let TDim::Add(terms) = self
121            && terms.len() >= 2
122            && let Some(parts) =
123                terms.iter().map(|t| t.maybe_div(other).ok()).collect::<Option<Vec<_>>>()
124            && let Some((_, q0)) = parts.first()
125            && parts.iter().all(|(_, q)| q == q0)
126        {
127            let q = *q0;
128            let sum = parts.into_iter().map(|(d, _)| d).fold(TDim::zero(), |acc, d| acc + d);
129            return Ok((sum.reduce(), q));
130        }
131        fn expand(dim: &TDim) -> (i64, Vec<TDim>) {
132            match dim {
133                TDim::Mul(terms) => terms.iter().map(expand).fold((1i64, vec![]), |acc, t| {
134                    (acc.0 * t.0, acc.1.into_iter().chain(t.1).collect())
135                }),
136                TDim::MulInt(a, terms) => {
137                    let (b, v) = expand(terms);
138                    (a * b, v)
139                }
140                TDim::Val(x) => (*x, vec![]),
141                TDim::Add(terms) => {
142                    let gcd =
143                        terms.iter().map(expand).map(|(n, _)| n).reduce(|a, b| a.gcd(&b)).unwrap();
144                    (
145                        gcd,
146                        vec![TDim::Add(terms.iter().map(|t| t.clone() / gcd).collect()).simplify()],
147                    )
148                }
149                it => (1, vec![it.clone()]),
150            }
151        }
152        let (mut num_int, mut num) = expand(self);
153        let (mut denum_int, mut denum) = expand(other);
154        if num == denum {
155            num = vec![];
156            denum = vec![];
157        }
158        for it in denum {
159            if let Some(pos) = num.iter().position(|n| n == &it) {
160                num.remove(pos);
161            } else {
162                bail!("Can't divide {} by {}", self, other)
163            }
164        }
165        use num_integer::Integer;
166        if denum_int < 0 {
167            num_int *= -1;
168            denum_int *= -1;
169        }
170        let gcd = num_int.gcd(&denum_int);
171        num_int /= gcd;
172        denum_int /= gcd;
173        Ok(((TDim::Mul(num) * num_int).reduce(), denum_int as u64))
174    }
175
176    fn to_i64(&self) -> Result<i64, TooEarly> {
177        TDim::to_i64(self)
178    }
179
180    fn as_i64(&self) -> Option<i64> {
181        TDim::as_i64(self)
182    }
183
184    fn eval(&self, values: &SymbolValues) -> Self {
185        self.eval(values)
186    }
187
188    fn substitute(&self, from: &Symbol, to: &Self) -> TractResult<Self> {
189        self.substitute(from, to)
190    }
191
192    fn substitute_all(&self, map: &std::collections::HashMap<Symbol, Self>) -> TractResult<Self> {
193        TDim::substitute_all(self, map)
194    }
195
196    fn eval_to_i64(&self, values: &SymbolValues) -> TractResult<i64> {
197        TDim::eval_to_i64(self, values)
198    }
199
200    fn broadcast(self, other: Self) -> TractResult<Self> {
201        if self.is_one() {
202            Ok(other)
203        } else if other.is_one() {
204            Ok(self)
205        } else {
206            Ok(TDim::Broadcast(vec![self, other]).simplify())
207        }
208    }
209
210    fn compatible_with(&self, other: &Self) -> bool {
211        self.compatible_with(other)
212    }
213
214    fn mini(self, other: Self) -> Self {
215        TDim::Min(vec![self, other]).simplify()
216    }
217
218    fn maxi(self, other: Self) -> Self {
219        TDim::Max(vec![self, other]).simplify()
220    }
221}
222
223impl<'a> std::convert::TryFrom<&'a TDim> for TDim {
224    type Error = TractError;
225    fn try_from(d: &'a TDim) -> TractResult<TDim> {
226        Ok(d.clone())
227    }
228}
229
230impl DimLike for usize {
231    fn maybe_div(&self, other: &Self) -> TractResult<(Self, u64)> {
232        use num_integer::Integer;
233        let gcd = self.gcd(other);
234        Ok((self / gcd, (other / gcd) as u64))
235    }
236
237    fn to_i64(&self) -> Result<i64, TooEarly> {
238        Ok(*self as i64)
239    }
240
241    fn as_i64(&self) -> Option<i64> {
242        Some(*self as i64)
243    }
244
245    fn eval(&self, _values: &SymbolValues) -> Self {
246        *self
247    }
248
249    fn substitute(&self, _from: &Symbol, _to: &Self) -> TractResult<Self> {
250        Ok(*self)
251    }
252
253    fn substitute_all(&self, _map: &std::collections::HashMap<Symbol, Self>) -> TractResult<Self> {
254        Ok(*self)
255    }
256
257    fn eval_to_i64(&self, _: &SymbolValues) -> TractResult<i64> {
258        Ok(*self as i64)
259    }
260
261    fn broadcast(self, other: Self) -> TractResult<Self> {
262        if self == 1 || self == other {
263            Ok(other)
264        } else if other == 1 {
265            Ok(self)
266        } else {
267            bail!("Can not broadcast {self} against {other}")
268        }
269    }
270
271    fn compatible_with(&self, other: &Self) -> bool {
272        self == other
273    }
274
275    fn mini(self, other: Self) -> Self {
276        if self < other { self } else { other }
277    }
278
279    fn maxi(self, other: Self) -> Self {
280        if self > other { self } else { other }
281    }
282}
283
284impl<'a> std::convert::TryFrom<&'a TDim> for usize {
285    type Error = TractError;
286    fn try_from(d: &'a TDim) -> TractResult<usize> {
287        Ok(d.to_usize()?)
288    }
289}
290
291/// Convenience trait to convert values to TDim.
292pub trait ToDim {
293    /// Convert self to a TDim.
294    fn to_dim(&self) -> TDim;
295}
296
297impl<I: Into<TDim> + Clone> ToDim for I {
298    fn to_dim(&self) -> TDim {
299        self.clone().into()
300    }
301}
302
303impl ToDim for &TDim {
304    fn to_dim(&self) -> TDim {
305        (*self).clone()
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    lazy_static::lazy_static! {
314        static ref S: (SymbolScope, Symbol) = {
315            let table = SymbolScope::default();
316            let s = table.new_with_prefix("S");
317            (table, s)
318        };
319    }
320
321    pub fn s() -> TDim {
322        S.1.clone().into()
323    }
324
325    #[test]
326    fn div() {
327        assert_eq!(TDim::from(12).maybe_div(&TDim::from(4)).unwrap(), (3.into(), 1));
328    }
329
330    #[test]
331    fn div_sym_int() {
332        assert_eq!((s() * 12).maybe_div(&TDim::from(4)).unwrap(), (s() * 3, 1));
333    }
334
335    #[test]
336    fn div_sym_sym() {
337        assert_eq!((s() * 12).maybe_div(&(s() * 4)).unwrap(), (3.into(), 1));
338    }
339
340    #[test]
341    fn div_sym_sym_ratio() {
342        assert_eq!((s() * 13).maybe_div(&(s() * 4)).unwrap(), (13.into(), 4));
343    }
344
345    #[test]
346    fn div_sym_sym_rem() {
347        assert!((s() + 1).maybe_div(&(s() * 4)).is_err());
348    }
349
350    #[test]
351    fn div_sym_sym_simply_1() {
352        assert_eq!((s()).maybe_div(&(s())).unwrap(), (TDim::Val(1), 1));
353    }
354
355    #[test]
356    fn div_sym_sym_complex() {
357        let s = s();
358        let b = S.0.sym("b");
359        assert_eq!(
360            (256.to_dim() * &s * &b).maybe_div(&(1.to_dim() * &s * &b)).unwrap(),
361            (256.into(), 1)
362        );
363    }
364
365    #[test]
366    fn div_sym_sym_with_add() {
367        assert_eq!((s() * 80 - 160).maybe_div(&(s() - 2)).unwrap(), (80.into(), 1));
368    }
369
370    #[test]
371    fn div_with_shared_div_ceil_factor() {
372        // Repro for:
373        //   Can't divide 8*(slice)*((T+7)/8)+8*(T+7)/8 by 8*(T+7)/8
374        //
375        // Numerator factors as (slice + 1) * 8 * ((T+7)/8); denominator is the
376        // shared 8 * ((T+7)/8), so the division should yield slice + 1.
377        let t: TDim = S.0.sym("T").into();
378        let slice = S.0.sym("slice");
379        let c = t.div_ceil(8); // (T+7)/8
380        let num = 8.to_dim() * &slice * &c + 8.to_dim() * &c;
381        let denom = 8.to_dim() * &c;
382        assert_eq!(num.maybe_div(&denom).unwrap(), (slice.to_dim() + 1, 1));
383    }
384}