Skip to main content

burn_std/data/
compare.rs

1use alloc::format;
2use alloc::string::String;
3use num_traits::{Float, ToPrimitive};
4
5use super::TensorData;
6use crate::{
7    BoolStore, DType, Element, ElementOrdered, bf16, f16, quantization::global_scale_dtype,
8};
9
10/// The tolerance used to compare to floating point numbers.
11///
12/// Generally, two numbers `x` and `y` are approximately equal if
13///
14/// ```text
15/// |x - y| < max(R * (|x + y|), A)
16/// ```
17///
18/// where `R` is the relative tolerance and `A` is the absolute tolerance.
19///
20///
21/// The most common way to initialize this struct is to use `Tolerance::<F>::default()`.
22/// In that case, the relative and absolute tolerances are computed using an heuristic based
23/// on the EPSILON and MIN_POSITIVE values of the given floating point type `F`.
24///
25/// Another common initialization is `Tolerance::<F>::rel_abs(1e-4, 1e-5).set_half_precision_relative(1e-2)`.
26/// This will use a sane default to manage values too close to 0.0 and
27/// use different relative tolerances depending on the floating point precision.
28#[derive(Debug, Clone, Copy)]
29pub struct Tolerance<F> {
30    relative: F,
31    absolute: F,
32}
33
34impl<F: Float> Default for Tolerance<F> {
35    fn default() -> Self {
36        Self::balanced()
37    }
38}
39
40impl<F: Float> Tolerance<F> {
41    /// Create a tolerance with strict precision setting.
42    pub fn strict() -> Self {
43        Self {
44            relative: F::from(0.00).unwrap(),
45            absolute: F::from(64).unwrap() * F::min_positive_value(),
46        }
47    }
48    /// Create a tolerance with balanced precision setting.
49    pub fn balanced() -> Self {
50        Self {
51            relative: F::from(0.005).unwrap(), // 0.5%
52            absolute: F::from(1e-5).unwrap(),
53        }
54    }
55
56    /// Create a tolerance with permissive precision setting.
57    pub fn permissive() -> Self {
58        Self {
59            relative: F::from(0.01).unwrap(), // 1.0%
60            absolute: F::from(0.01).unwrap(),
61        }
62    }
63    /// When comparing two numbers, this uses both the relative and absolute differences.
64    ///
65    /// That is, `x` and `y` are approximately equal if
66    ///
67    /// ```text
68    /// |x - y| < max(R * (|x + y|), A)
69    /// ```
70    ///
71    /// where `R` is the `relative` tolerance and `A` is the `absolute` tolerance.
72    pub fn rel_abs<FF: ToPrimitive>(relative: FF, absolute: FF) -> Self {
73        let relative = Self::check_relative(relative);
74        let absolute = Self::check_absolute(absolute);
75
76        Self { relative, absolute }
77    }
78
79    /// When comparing two numbers, this uses only the relative difference.
80    ///
81    /// That is, `x` and `y` are approximately equal if
82    ///
83    /// ```text
84    /// |x - y| < R * max(|x|, |y|)
85    /// ```
86    ///
87    /// where `R` is the relative `tolerance`.
88    pub fn relative<FF: ToPrimitive>(tolerance: FF) -> Self {
89        let relative = Self::check_relative(tolerance);
90
91        Self {
92            relative,
93            absolute: F::from(0.0).unwrap(),
94        }
95    }
96
97    /// When comparing two numbers, this uses only the absolute difference.
98    ///
99    /// That is, `x` and `y` are approximately equal if
100    ///
101    /// ```text
102    /// |x - y| < A
103    /// ```
104    ///
105    /// where `A` is the absolute `tolerance`.
106    pub fn absolute<FF: ToPrimitive>(tolerance: FF) -> Self {
107        let absolute = Self::check_absolute(tolerance);
108
109        Self {
110            relative: F::from(0.0).unwrap(),
111            absolute,
112        }
113    }
114
115    /// Returns the relative tolerance.
116    pub fn get_relative(&self) -> F {
117        self.relative
118    }
119
120    /// Returns the absolute tolerance.
121    pub fn get_absolute(&self) -> F {
122        self.absolute
123    }
124
125    /// Change the relative tolerance to the given one.
126    pub fn set_relative<FF: ToPrimitive>(mut self, tolerance: FF) -> Self {
127        self.relative = Self::check_relative(tolerance);
128        self
129    }
130
131    /// Change the relative tolerance to the given one only if `F` is half precision.
132    pub fn set_half_precision_relative<FF: ToPrimitive>(mut self, tolerance: FF) -> Self {
133        if core::mem::size_of::<F>() == 2 {
134            self.relative = Self::check_relative(tolerance);
135        }
136        self
137    }
138
139    /// Change the relative tolerance to the given one only if `F` is single precision.
140    pub fn set_single_precision_relative<FF: ToPrimitive>(mut self, tolerance: FF) -> Self {
141        if core::mem::size_of::<F>() == 4 {
142            self.relative = Self::check_relative(tolerance);
143        }
144        self
145    }
146
147    /// Change the relative tolerance to the given one only if `F` is double precision.
148    pub fn set_double_precision_relative<FF: ToPrimitive>(mut self, tolerance: FF) -> Self {
149        if core::mem::size_of::<F>() == 8 {
150            self.relative = Self::check_relative(tolerance);
151        }
152        self
153    }
154
155    /// Change the absolute tolerance to the given one.
156    pub fn set_absolute<FF: ToPrimitive>(mut self, tolerance: FF) -> Self {
157        self.absolute = Self::check_absolute(tolerance);
158        self
159    }
160
161    /// Change the absolute tolerance to the given one only if `F` is half precision.
162    pub fn set_half_precision_absolute<FF: ToPrimitive>(mut self, tolerance: FF) -> Self {
163        if core::mem::size_of::<F>() == 2 {
164            self.absolute = Self::check_absolute(tolerance);
165        }
166        self
167    }
168
169    /// Change the absolute tolerance to the given one only if `F` is single precision.
170    pub fn set_single_precision_absolute<FF: ToPrimitive>(mut self, tolerance: FF) -> Self {
171        if core::mem::size_of::<F>() == 4 {
172            self.absolute = Self::check_absolute(tolerance);
173        }
174        self
175    }
176
177    /// Change the absolute tolerance to the given one only if `F` is double precision.
178    pub fn set_double_precision_absolute<FF: ToPrimitive>(mut self, tolerance: FF) -> Self {
179        if core::mem::size_of::<F>() == 8 {
180            self.absolute = Self::check_absolute(tolerance);
181        }
182        self
183    }
184
185    /// Checks if `x` and `y` are approximately equal given the tolerance.
186    pub fn approx_eq(&self, x: F, y: F) -> bool {
187        // See the accepted answer here
188        // https://stackoverflow.com/questions/4915462/how-should-i-do-floating-point-comparison
189
190        // This also handles the case where both a and b are infinity so that we don't need
191        // to manage it in the rest of the function.
192        if x == y {
193            return true;
194        }
195
196        let diff = (x - y).abs();
197        let max = F::max(x.abs(), y.abs());
198
199        diff < self.absolute.max(self.relative * max)
200    }
201
202    fn check_relative<FF: ToPrimitive>(tolerance: FF) -> F {
203        let tolerance = F::from(tolerance).unwrap();
204        assert!(tolerance <= F::one());
205        tolerance
206    }
207
208    fn check_absolute<FF: ToPrimitive>(tolerance: FF) -> F {
209        let tolerance = F::from(tolerance).unwrap();
210        assert!(tolerance >= F::zero());
211        tolerance
212    }
213}
214
215impl TensorData {
216    /// Asserts the data is equal to another data.
217    /// Shapes, element counts, and values must match.
218    ///
219    /// # Arguments
220    ///
221    /// * `other` - The other data.
222    /// * `strict` - If true, the data types must the be same.
223    ///   Otherwise, the comparison is done in the current data type.
224    ///
225    /// # Panics
226    ///
227    /// Panics if the data is not equal.
228    #[track_caller]
229    pub fn assert_eq(&self, other: &Self, strict: bool) {
230        if strict {
231            assert_eq!(
232                self.dtype, other.dtype,
233                "Data types differ ({:?} != {:?})",
234                self.dtype, other.dtype
235            );
236        }
237
238        match self.dtype {
239            DType::F64 => self.assert_eq_elem::<f64>(other),
240            DType::F32 | DType::Flex32 => self.assert_eq_elem::<f32>(other),
241            DType::F16 => self.assert_eq_elem::<f16>(other),
242            DType::BF16 => self.assert_eq_elem::<bf16>(other),
243            DType::I64 => self.assert_eq_elem::<i64>(other),
244            DType::I32 => self.assert_eq_elem::<i32>(other),
245            DType::I16 => self.assert_eq_elem::<i16>(other),
246            DType::I8 => self.assert_eq_elem::<i8>(other),
247            DType::U64 => self.assert_eq_elem::<u64>(other),
248            DType::U32 => self.assert_eq_elem::<u32>(other),
249            DType::U16 => self.assert_eq_elem::<u16>(other),
250            DType::U8 => self.assert_eq_elem::<u8>(other),
251            DType::Bool(BoolStore::Native) => self.assert_eq_elem::<bool>(other),
252            DType::Bool(BoolStore::U8) => self.assert_eq_elem::<u8>(other),
253            DType::Bool(BoolStore::U32) => self.assert_eq_elem::<u32>(other),
254            DType::QFloat(q) => {
255                // Strict or not, it doesn't make sense to compare quantized data to not quantized data for equality
256                let q_other = if let DType::QFloat(q_other) = other.dtype {
257                    q_other
258                } else {
259                    panic!("Quantized data differs from other not quantized data")
260                };
261
262                // Data equality mostly depends on input quantization type, but we also check levels
263                if q.value == q_other.value
264                    && q.block_size() == q_other.block_size()
265                    && global_scale_dtype(&q) == global_scale_dtype(&q_other)
266                {
267                    self.assert_eq_elem::<i8>(other)
268                } else {
269                    panic!("Quantization schemes differ ({q:?} != {q_other:?})")
270                }
271            }
272        }
273    }
274
275    #[track_caller]
276    fn assert_eq_elem<E: Element>(&self, other: &Self) {
277        let mut message = String::new();
278        if self.shape != other.shape {
279            message += format!(
280                "\n  => Shape is different: {:?} != {:?}",
281                self.shape, other.shape
282            )
283            .as_str();
284        }
285
286        // Count the stored elements: num_elements() only reflects the declared shape.
287        let iter_self = self.iter_exact::<E>();
288        let iter_other = other.iter_exact::<E>();
289        let len_self = iter_self.len();
290        let len_other = iter_other.len();
291        if len_self != len_other {
292            message += format!("\n  => Element counts differ: {len_self} != {len_other}").as_str();
293        }
294
295        let mut num_diff = 0;
296        let max_num_diff = 5;
297        for (i, (a, b)) in iter_self.zip(iter_other).enumerate() {
298            if !a.eq(&b) {
299                // Only print the first 5 different values.
300                if num_diff < max_num_diff {
301                    message += format!("\n  => Position {i}: {a} != {b}").as_str();
302                }
303                num_diff += 1;
304            }
305        }
306
307        if num_diff >= max_num_diff {
308            message += format!("\n{} more errors...", num_diff - max_num_diff).as_str();
309        }
310
311        if !message.is_empty() {
312            panic!("Tensors are not eq:{message}");
313        }
314    }
315
316    /// Asserts the data is approximately equal to another data.
317    ///
318    /// # Arguments
319    ///
320    /// * `other` - The other data.
321    /// * `tolerance` - The tolerance of the comparison.
322    ///
323    /// # Panics
324    ///
325    /// Panics if the data is not approximately equal.
326    #[track_caller]
327    pub fn assert_approx_eq<F: Float + Element>(&self, other: &Self, tolerance: Tolerance<F>) {
328        let mut message = String::new();
329        if self.shape != other.shape {
330            message += format!(
331                "\n  => Shape is different: {:?} != {:?}",
332                self.shape, other.shape
333            )
334            .as_str();
335        }
336
337        let iter = self.iter::<F>().zip(other.iter::<F>());
338
339        let mut num_diff = 0;
340        let max_num_diff = 5;
341
342        for (i, (a, b)) in iter.enumerate() {
343            //if they are both nan, then they are equally nan
344            let both_nan = a.is_nan() && b.is_nan();
345            //this works for both infinities
346            let both_inf =
347                a.is_infinite() && b.is_infinite() && ((a > F::zero()) == (b > F::zero()));
348
349            if both_nan || both_inf {
350                continue;
351            }
352
353            if !tolerance.approx_eq(F::from(a).unwrap(), F::from(b).unwrap()) {
354                // Only print the first 5 different values.
355                if num_diff < max_num_diff {
356                    let diff_abs = ToPrimitive::to_f64(&(a - b).abs()).unwrap();
357                    let max = F::max(a.abs(), b.abs());
358                    let diff_rel = diff_abs / ToPrimitive::to_f64(&max).unwrap();
359
360                    let tol_rel = ToPrimitive::to_f64(&tolerance.relative).unwrap();
361                    let tol_abs = ToPrimitive::to_f64(&tolerance.absolute).unwrap();
362
363                    message += format!(
364                        "\n  => Position {i}: {a} != {b}\n     diff (rel = {diff_rel:+.2e}, abs = {diff_abs:+.2e}), tol (rel = {tol_rel:+.2e}, abs = {tol_abs:+.2e})"
365                    )
366                    .as_str();
367                }
368                num_diff += 1;
369            }
370        }
371
372        if num_diff >= max_num_diff {
373            message += format!("\n{} more errors...", num_diff - 5).as_str();
374        }
375
376        if !message.is_empty() {
377            panic!("Tensors are not approx eq:{message}");
378        }
379    }
380
381    /// Asserts each value is within a given range.
382    ///
383    /// # Arguments
384    ///
385    /// * `range` - The range.
386    ///
387    /// # Panics
388    ///
389    /// If any value is not within the half-open range bounded inclusively below
390    /// and exclusively above (`start..end`).
391    pub fn assert_within_range<E: ElementOrdered>(&self, range: core::ops::Range<E>) {
392        for elem in self.iter::<E>() {
393            if elem.cmp(&range.start).is_lt() || elem.cmp(&range.end).is_ge() {
394                panic!("Element ({elem:?}) is not within range {range:?}");
395            }
396        }
397    }
398
399    /// Asserts each value is within a given inclusive range.
400    ///
401    /// # Arguments
402    ///
403    /// * `range` - The range.
404    ///
405    /// # Panics
406    ///
407    /// If any value is not within the half-open range bounded inclusively (`start..=end`).
408    pub fn assert_within_range_inclusive<E: ElementOrdered>(
409        &self,
410        range: core::ops::RangeInclusive<E>,
411    ) {
412        let start = range.start();
413        let end = range.end();
414
415        for elem in self.iter::<E>() {
416            if elem.cmp(start).is_lt() || elem.cmp(end).is_gt() {
417                panic!("Element ({elem:?}) is not within range {range:?}");
418            }
419        }
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426    use alloc::vec;
427
428    #[test]
429    #[should_panic(expected = "Element counts differ")]
430    fn should_assert_eq_reject_shorter_data() {
431        // The constructors reject this, but quantized data skips the length check and in-crate
432        // code can write the fields directly, so the comparison keeps its own count check.
433        let data =
434            TensorData::from_bytes_unchecked(TensorData::from([1.0f32]).bytes, [2], DType::F32);
435        let expected = TensorData::from([1.0f32, 2.0]);
436
437        data.assert_eq(&expected, false);
438    }
439
440    #[test]
441    #[should_panic(expected = "Element counts differ")]
442    fn should_assert_eq_reject_longer_data() {
443        let data = TensorData::from([1.0f32, 2.0]);
444        let expected =
445            TensorData::from_bytes_unchecked(TensorData::from([1.0f32]).bytes, [2], DType::F32);
446
447        data.assert_eq(&expected, true);
448    }
449
450    #[test]
451    #[should_panic(expected = "Element counts differ")]
452    fn should_assert_eq_reject_empty_data_with_nonempty_shape() {
453        let data =
454            TensorData::from_bytes_unchecked(crate::Bytes::from_bytes_vec(vec![]), [1], DType::F32);
455        let expected = TensorData::from([1.0f32]);
456
457        data.assert_eq(&expected, false);
458    }
459
460    #[test]
461    #[should_panic(expected = "Element counts differ")]
462    fn should_assert_eq_reject_different_counts_with_equal_byte_lengths() {
463        let data =
464            TensorData::from_bytes_unchecked(TensorData::from([1.0f64]).bytes, [2], DType::F64);
465        let expected = TensorData::from([1.0f32, 2.0]);
466
467        data.assert_eq(&expected, false);
468    }
469
470    #[test]
471    fn should_assert_eq_allow_different_dtypes_when_not_strict() {
472        let data = TensorData::from([1.0f32, 2.0]);
473        let expected = TensorData::from([1i64, 2]);
474
475        data.assert_eq(&expected, false);
476        expected.assert_eq(&data, false);
477    }
478
479    #[test]
480    fn should_assert_appox_eq_limit() {
481        let data1 = TensorData::from([[3.0, 5.0, 6.0]]);
482        let data2 = TensorData::from([[3.03, 5.0, 6.0]]);
483
484        data1.assert_approx_eq::<f32>(&data2, Tolerance::absolute(3e-2));
485        data1.assert_approx_eq::<f16>(&data2, Tolerance::absolute(3e-2));
486    }
487
488    #[test]
489    #[should_panic]
490    fn should_assert_approx_eq_above_limit() {
491        let data1 = TensorData::from([[3.0, 5.0, 6.0]]);
492        let data2 = TensorData::from([[3.031, 5.0, 6.0]]);
493
494        data1.assert_approx_eq::<f32>(&data2, Tolerance::absolute(1e-2));
495    }
496
497    #[test]
498    #[should_panic]
499    fn should_assert_approx_eq_check_shape() {
500        let data1 = TensorData::from([[3.0, 5.0, 6.0, 7.0]]);
501        let data2 = TensorData::from([[3.0, 5.0, 6.0]]);
502
503        data1.assert_approx_eq::<f32>(&data2, Tolerance::absolute(1e-2));
504    }
505}