1176 lines
38 KiB
Python
1176 lines
38 KiB
Python
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from __future__ import annotations
|
|
|
|
import abc
|
|
import cmath
|
|
import collections.abc
|
|
import contextlib
|
|
from typing import TYPE_CHECKING, Any, NoReturn
|
|
|
|
import numpy as np
|
|
|
|
import paddle
|
|
from paddle.base.data_feeder import promote_types
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable, Collection, Sequence
|
|
|
|
|
|
class ErrorMeta(Exception):
|
|
"""Internal testing exception that makes that carries error metadata."""
|
|
|
|
def __init__(
|
|
self, type: type[Exception], msg: str, *, id: tuple[Any, ...] = ()
|
|
) -> None:
|
|
super().__init__(
|
|
"If you are a user and see this message during normal operation, "
|
|
"it implies a mismatch found by paddle.testing.assert_close."
|
|
)
|
|
self.type = type
|
|
self.msg = msg
|
|
self.id = id
|
|
|
|
def to_error(
|
|
self, msg: str | Callable[[str], str] | None = None
|
|
) -> Exception:
|
|
if not isinstance(msg, str):
|
|
generated_msg = self.msg
|
|
if self.id:
|
|
generated_msg += f"\n\nThe failure occurred for item {''.join(str([item]) for item in self.id)}"
|
|
|
|
msg = msg(generated_msg) if callable(msg) else generated_msg
|
|
|
|
return self.type(msg)
|
|
|
|
|
|
# {dtype: (rtol, atol)}
|
|
_DTYPE_PRECISIONS = {
|
|
paddle.float16: (0.001, 1e-5),
|
|
paddle.float32: (1.3e-6, 1e-5),
|
|
paddle.float64: (1e-7, 1e-7),
|
|
paddle.complex64: (1.3e-6, 1e-5),
|
|
paddle.complex128: (1e-7, 1e-7),
|
|
}
|
|
|
|
if hasattr(paddle, "bfloat16"):
|
|
_DTYPE_PRECISIONS[paddle.bfloat16] = (0.016, 1e-5)
|
|
|
|
_QUANTIZED_TYPES = [
|
|
getattr(paddle, t) for t in ["int8", "int16", "uint8"] if hasattr(paddle, t)
|
|
]
|
|
for q_type in _QUANTIZED_TYPES:
|
|
_DTYPE_PRECISIONS[q_type] = _DTYPE_PRECISIONS[paddle.float32]
|
|
|
|
|
|
def default_tolerances(
|
|
*inputs: paddle.Tensor | paddle.dtype,
|
|
dtype_precisions: dict[paddle.dtype, tuple[float, float]] | None = None,
|
|
) -> tuple[float, float]:
|
|
"""Returns the default absolute and relative testing tolerances."""
|
|
dtypes = []
|
|
for input in inputs:
|
|
if isinstance(input, paddle.Tensor):
|
|
dtypes.append(input.dtype)
|
|
elif isinstance(input, paddle.dtype):
|
|
dtypes.append(input)
|
|
else:
|
|
raise TypeError(
|
|
f"Expected a paddle.Tensor or a paddle.dtype, but got {type(input)} instead."
|
|
)
|
|
dtype_precisions = dtype_precisions or _DTYPE_PRECISIONS
|
|
rtols, atols = zip(
|
|
*[dtype_precisions.get(dtype, (0.0, 0.0)) for dtype in dtypes]
|
|
)
|
|
return max(rtols), max(atols)
|
|
|
|
|
|
def get_tolerances(
|
|
*inputs: paddle.Tensor | paddle.dtype,
|
|
rtol: float | None,
|
|
atol: float | None,
|
|
id: tuple[Any, ...] = (),
|
|
) -> tuple[float, float]:
|
|
"""Gets absolute and relative to be used for numeric comparisons."""
|
|
if (rtol is None) ^ (atol is None):
|
|
raise ErrorMeta(
|
|
ValueError,
|
|
f"Both 'rtol' and 'atol' must be either specified or omitted, "
|
|
f"but got no {'rtol' if rtol is None else 'atol'}.",
|
|
id=id,
|
|
)
|
|
elif rtol is not None and atol is not None:
|
|
return rtol, atol
|
|
else:
|
|
return default_tolerances(*inputs)
|
|
|
|
|
|
def _make_mismatch_msg(
|
|
*,
|
|
default_identifier: str,
|
|
identifier: str | Callable[[str], str] | None = None,
|
|
extra: str | None = None,
|
|
abs_diff: float,
|
|
abs_diff_idx: int | tuple[int, ...] | None = None,
|
|
atol: float,
|
|
rel_diff: float,
|
|
rel_diff_idx: int | tuple[int, ...] | None = None,
|
|
rtol: float,
|
|
) -> str:
|
|
equality = rtol == 0 and atol == 0
|
|
|
|
def make_diff_msg(
|
|
*,
|
|
type: str,
|
|
diff: float,
|
|
idx: int | tuple[int, ...] | None,
|
|
tol: float,
|
|
) -> str:
|
|
if idx is None:
|
|
msg = f"{type.title()} difference: {diff}"
|
|
else:
|
|
msg = f"Greatest {type} difference: {diff} at index {idx}"
|
|
if not equality:
|
|
msg += f" (up to {tol} allowed)"
|
|
return msg + "\n"
|
|
|
|
if identifier is None:
|
|
identifier = default_identifier
|
|
elif callable(identifier):
|
|
identifier = identifier(default_identifier)
|
|
|
|
msg = f"{identifier} are not {'equal' if equality else 'close'}!\n\n"
|
|
|
|
if extra:
|
|
msg += f"{extra.strip()}\n"
|
|
|
|
msg += make_diff_msg(
|
|
type="absolute", diff=abs_diff, idx=abs_diff_idx, tol=atol
|
|
)
|
|
msg += make_diff_msg(
|
|
type="relative", diff=rel_diff, idx=rel_diff_idx, tol=rtol
|
|
)
|
|
|
|
return msg.strip()
|
|
|
|
|
|
def make_scalar_mismatch_msg(
|
|
actual: bool | complex,
|
|
expected: bool | complex,
|
|
*,
|
|
rtol: float,
|
|
atol: float,
|
|
identifier: str | Callable[[str], str] | None = None,
|
|
) -> str:
|
|
abs_diff = abs(actual - expected)
|
|
rel_diff = float("inf") if expected == 0 else abs_diff / abs(expected)
|
|
return _make_mismatch_msg(
|
|
default_identifier="Scalars",
|
|
identifier=identifier,
|
|
extra=f"Expected {expected} but got {actual}.",
|
|
abs_diff=abs_diff,
|
|
atol=atol,
|
|
rel_diff=rel_diff,
|
|
rtol=rtol,
|
|
)
|
|
|
|
|
|
def make_tensor_mismatch_msg(
|
|
actual: paddle.Tensor,
|
|
expected: paddle.Tensor,
|
|
matches: paddle.Tensor,
|
|
*,
|
|
rtol: float,
|
|
atol: float,
|
|
identifier: str | Callable[[str], str] | None = None,
|
|
):
|
|
def unravel_flat_index(flat_index: int) -> tuple[int, ...]:
|
|
if not matches.shape:
|
|
return ()
|
|
|
|
inverse_index = []
|
|
for size in matches.shape[::-1]:
|
|
div, mod = divmod(flat_index, size)
|
|
flat_index = div
|
|
inverse_index.append(mod)
|
|
|
|
return tuple(inverse_index[::-1])
|
|
|
|
number_of_elements = matches.numel().item()
|
|
total_mismatches = number_of_elements - int(
|
|
paddle.sum(matches.astype("int64")).item()
|
|
)
|
|
extra = (
|
|
f"Mismatched elements: {total_mismatches} / {number_of_elements} "
|
|
f"({total_mismatches / number_of_elements:.1%})"
|
|
)
|
|
|
|
actual_flat = actual.flatten()
|
|
expected_flat = expected.flatten()
|
|
matches_flat = matches.flatten()
|
|
|
|
if (
|
|
actual.dtype
|
|
not in [
|
|
paddle.float16,
|
|
paddle.float32,
|
|
paddle.float64,
|
|
paddle.complex64,
|
|
paddle.complex128,
|
|
]
|
|
and hasattr(paddle, "bfloat16")
|
|
and actual.dtype != paddle.bfloat16
|
|
):
|
|
actual_flat = actual_flat.astype("int64")
|
|
expected_flat = expected_flat.astype("int64")
|
|
|
|
abs_diff = paddle.abs(actual_flat - expected_flat)
|
|
abs_diff = paddle.where(matches_flat, paddle.zeros_like(abs_diff), abs_diff)
|
|
|
|
max_abs_diff = paddle.max(abs_diff)
|
|
max_abs_diff_flat_idx = paddle.argmax(abs_diff)
|
|
|
|
rel_diff = abs_diff / paddle.abs(expected_flat)
|
|
rel_diff = paddle.where(matches_flat, paddle.zeros_like(rel_diff), rel_diff)
|
|
|
|
max_rel_diff = paddle.max(rel_diff)
|
|
max_rel_diff_flat_idx = paddle.argmax(rel_diff)
|
|
|
|
return _make_mismatch_msg(
|
|
default_identifier="Tensor-likes",
|
|
identifier=identifier,
|
|
extra=extra,
|
|
abs_diff=max_abs_diff.item(),
|
|
abs_diff_idx=unravel_flat_index(int(max_abs_diff_flat_idx)),
|
|
atol=atol,
|
|
rel_diff=max_rel_diff.item(),
|
|
rel_diff_idx=unravel_flat_index(int(max_rel_diff_flat_idx)),
|
|
rtol=rtol,
|
|
)
|
|
|
|
|
|
class UnsupportedInputs(Exception):
|
|
"""Exception to be raised during the construction of a :class:`Pair` in case it doesn't support the inputs."""
|
|
|
|
|
|
class Pair(abc.ABC):
|
|
def __init__(
|
|
self,
|
|
actual: Any,
|
|
expected: Any,
|
|
*,
|
|
id: tuple[Any, ...] = (),
|
|
**unknown_parameters: Any,
|
|
) -> None:
|
|
self.actual = actual
|
|
self.expected = expected
|
|
self.id = id
|
|
self._unknown_parameters = unknown_parameters
|
|
|
|
@staticmethod
|
|
def _inputs_not_supported() -> NoReturn:
|
|
raise UnsupportedInputs
|
|
|
|
@staticmethod
|
|
def _check_inputs_isinstance(*inputs: Any, cls: type | tuple[type, ...]):
|
|
if not all(isinstance(input, cls) for input in inputs):
|
|
Pair._inputs_not_supported()
|
|
|
|
def _fail(
|
|
self, type: type[Exception], msg: str, *, id: tuple[Any, ...] = ()
|
|
) -> NoReturn:
|
|
raise ErrorMeta(
|
|
type, msg, id=self.id if not id and hasattr(self, "id") else id
|
|
)
|
|
|
|
@abc.abstractmethod
|
|
def compare(self) -> None:
|
|
"""Compares the inputs and raises an :class`ErrorMeta` in case they mismatch."""
|
|
|
|
def extra_repr(self) -> Sequence[str | tuple[str, Any]]:
|
|
return []
|
|
|
|
def __repr__(self) -> str:
|
|
head = f"{type(self).__name__}("
|
|
tail = ")"
|
|
body = [
|
|
f" {name}={value!s},"
|
|
for name, value in [
|
|
("id", self.id),
|
|
("actual", self.actual),
|
|
("expected", self.expected),
|
|
*[
|
|
(extra, getattr(self, extra))
|
|
if isinstance(extra, str)
|
|
else extra
|
|
for extra in self.extra_repr()
|
|
],
|
|
]
|
|
]
|
|
return "\n".join((head, *body, *tail))
|
|
|
|
|
|
class ObjectPair(Pair):
|
|
"""Pair for any type of inputs that will be compared with the `==` operator."""
|
|
|
|
def compare(self) -> None:
|
|
try:
|
|
equal = self.actual == self.expected
|
|
except Exception as error:
|
|
raise ErrorMeta(
|
|
ValueError,
|
|
f"{self.actual} == {self.expected} failed with:\n{error}.",
|
|
id=self.id,
|
|
) from error
|
|
|
|
if not equal:
|
|
self._fail(AssertionError, f"{self.actual} != {self.expected}")
|
|
|
|
|
|
class NonePair(Pair):
|
|
"""Pair for ``None`` inputs."""
|
|
|
|
def __init__(
|
|
self, actual: Any, expected: Any, **other_parameters: Any
|
|
) -> None:
|
|
if not (actual is None or expected is None):
|
|
self._inputs_not_supported()
|
|
|
|
super().__init__(actual, expected, **other_parameters)
|
|
|
|
def compare(self) -> None:
|
|
if not (self.actual is None and self.expected is None):
|
|
self._fail(
|
|
AssertionError,
|
|
f"None mismatch: {self.actual} is not {self.expected}",
|
|
)
|
|
|
|
|
|
class BooleanPair(Pair):
|
|
"""Pair for :class:`bool` inputs."""
|
|
|
|
def __init__(
|
|
self,
|
|
actual: Any,
|
|
expected: Any,
|
|
*,
|
|
id: tuple[Any, ...],
|
|
**other_parameters: Any,
|
|
) -> None:
|
|
actual, expected = self._process_inputs(actual, expected, id=id)
|
|
super().__init__(actual, expected, **other_parameters)
|
|
|
|
@property
|
|
def _supported_types(self) -> tuple[type, ...]:
|
|
cls: list[type] = [bool]
|
|
cls.append(np.bool_)
|
|
return tuple(cls)
|
|
|
|
def _process_inputs(
|
|
self, actual: Any, expected: Any, *, id: tuple[Any, ...]
|
|
) -> tuple[bool, bool]:
|
|
self._check_inputs_isinstance(
|
|
actual, expected, cls=self._supported_types
|
|
)
|
|
actual, expected = (
|
|
self._to_bool(bool_like, id=id) for bool_like in (actual, expected)
|
|
)
|
|
return actual, expected
|
|
|
|
def _to_bool(self, bool_like: Any, *, id: tuple[Any, ...]) -> bool:
|
|
if isinstance(bool_like, bool):
|
|
return bool_like
|
|
elif isinstance(bool_like, np.bool_):
|
|
return bool_like.item()
|
|
else:
|
|
raise ErrorMeta(
|
|
TypeError, f"Unknown boolean type {type(bool_like)}.", id=id
|
|
)
|
|
|
|
def compare(self) -> None:
|
|
if self.actual is not self.expected:
|
|
self._fail(
|
|
AssertionError,
|
|
f"Booleans mismatch: {self.actual} is not {self.expected}",
|
|
)
|
|
|
|
|
|
class NumberPair(Pair):
|
|
"""Pair for Python number inputs."""
|
|
|
|
_TYPE_TO_DTYPE = {
|
|
int: paddle.int64,
|
|
float: paddle.float64,
|
|
complex: paddle.complex128,
|
|
}
|
|
_NUMBER_TYPES = tuple(_TYPE_TO_DTYPE.keys())
|
|
|
|
def __init__(
|
|
self,
|
|
actual: Any,
|
|
expected: Any,
|
|
*,
|
|
id: tuple[Any, ...] = (),
|
|
rtol: float | None = None,
|
|
atol: float | None = None,
|
|
equal_nan: bool = False,
|
|
check_dtype: bool = False,
|
|
**other_parameters: Any,
|
|
) -> None:
|
|
actual, expected = self._process_inputs(actual, expected, id=id)
|
|
super().__init__(actual, expected, id=id, **other_parameters)
|
|
|
|
self.rtol, self.atol = get_tolerances(
|
|
*[self._TYPE_TO_DTYPE[type(input)] for input in (actual, expected)],
|
|
rtol=rtol,
|
|
atol=atol,
|
|
id=id,
|
|
)
|
|
self.equal_nan = equal_nan
|
|
self.check_dtype = check_dtype
|
|
|
|
@property
|
|
def _supported_types(self) -> tuple[type, ...]:
|
|
cls = list(self._NUMBER_TYPES)
|
|
cls.append(np.number)
|
|
return tuple(cls)
|
|
|
|
def _process_inputs(
|
|
self, actual: Any, expected: Any, *, id: tuple[Any, ...]
|
|
) -> tuple[int | float | complex, int | float | complex]:
|
|
self._check_inputs_isinstance(
|
|
actual, expected, cls=self._supported_types
|
|
)
|
|
actual, expected = (
|
|
self._to_number(number_like, id=id)
|
|
for number_like in (actual, expected)
|
|
)
|
|
return actual, expected
|
|
|
|
def _to_number(
|
|
self, number_like: Any, *, id: tuple[Any, ...]
|
|
) -> int | float | complex:
|
|
if isinstance(number_like, np.number):
|
|
return number_like.item()
|
|
elif isinstance(number_like, self._NUMBER_TYPES):
|
|
return number_like
|
|
else:
|
|
raise ErrorMeta(
|
|
TypeError, f"Unknown number type {type(number_like)}.", id=id
|
|
)
|
|
|
|
def compare(self) -> None:
|
|
if self.check_dtype and type(self.actual) is not type(self.expected):
|
|
self._fail(
|
|
AssertionError,
|
|
f"The (d)types do not match: {type(self.actual)} != {type(self.expected)}.",
|
|
)
|
|
|
|
if self.actual == self.expected:
|
|
return
|
|
|
|
if (
|
|
self.equal_nan
|
|
and cmath.isnan(self.actual)
|
|
and cmath.isnan(self.expected)
|
|
):
|
|
return
|
|
|
|
abs_diff = abs(self.actual - self.expected)
|
|
tolerance = self.atol + self.rtol * abs(self.expected)
|
|
|
|
if cmath.isfinite(abs_diff) and abs_diff <= tolerance:
|
|
return
|
|
|
|
self._fail(
|
|
AssertionError,
|
|
make_scalar_mismatch_msg(
|
|
self.actual, self.expected, rtol=self.rtol, atol=self.atol
|
|
),
|
|
)
|
|
|
|
def extra_repr(self) -> Sequence[str]:
|
|
return (
|
|
"rtol",
|
|
"atol",
|
|
"equal_nan",
|
|
"check_dtype",
|
|
)
|
|
|
|
|
|
class StaticPair(Pair):
|
|
def __init__(
|
|
self,
|
|
actual: Any,
|
|
expected: Any,
|
|
check_dtype: bool = True,
|
|
**other_parameters: Any,
|
|
) -> None:
|
|
is_paddle_pir = isinstance(actual, paddle.pir.Value) or isinstance(
|
|
expected, paddle.pir.Value
|
|
)
|
|
|
|
if not is_paddle_pir:
|
|
self._inputs_not_supported()
|
|
|
|
super().__init__(actual, expected, **other_parameters)
|
|
self.check_dtype = check_dtype
|
|
|
|
def compare(self) -> None:
|
|
if type(self.actual) is not type(self.expected):
|
|
self._fail(
|
|
AssertionError,
|
|
f"The Python types do not match: {type(self.actual)} != {type(self.expected)}.",
|
|
)
|
|
|
|
if self.check_dtype and self.actual.dtype != self.expected.dtype:
|
|
self._fail(
|
|
AssertionError,
|
|
f"The values for attribute dtype do not match: {self.actual.dtype} != {self.expected.dtype}.",
|
|
)
|
|
|
|
act_shape = self.actual.shape
|
|
exp_shape = self.expected.shape
|
|
shape_match = True
|
|
|
|
if len(act_shape) != len(exp_shape):
|
|
shape_match = False
|
|
else:
|
|
for a_dim, e_dim in zip(act_shape, exp_shape):
|
|
if a_dim != -1 and e_dim != -1 and a_dim != e_dim:
|
|
shape_match = False
|
|
break
|
|
|
|
if not shape_match:
|
|
self._fail(
|
|
AssertionError,
|
|
f"The values for attribute shape do not match: {act_shape} != {exp_shape}.",
|
|
)
|
|
|
|
|
|
class TensorLikePair(Pair):
|
|
"""Pair for :class:`paddle.Tensor`-like inputs."""
|
|
|
|
def __init__(
|
|
self,
|
|
actual: Any,
|
|
expected: Any,
|
|
*,
|
|
id: tuple[Any, ...] = (),
|
|
allow_subclasses: bool = True,
|
|
rtol: float | None = None,
|
|
atol: float | None = None,
|
|
equal_nan: bool = False,
|
|
check_device: bool = True,
|
|
check_dtype: bool = True,
|
|
**other_parameters: Any,
|
|
):
|
|
actual, expected = self._process_inputs(
|
|
actual, expected, id=id, allow_subclasses=allow_subclasses
|
|
)
|
|
super().__init__(actual, expected, id=id, **other_parameters)
|
|
|
|
self.rtol, self.atol = get_tolerances(
|
|
actual, expected, rtol=rtol, atol=atol, id=self.id
|
|
)
|
|
self.equal_nan = equal_nan
|
|
self.check_device = check_device
|
|
self.check_dtype = check_dtype
|
|
|
|
def _process_inputs(
|
|
self,
|
|
actual: Any,
|
|
expected: Any,
|
|
*,
|
|
id: tuple[Any, ...],
|
|
allow_subclasses: bool,
|
|
) -> tuple[paddle.Tensor, paddle.Tensor]:
|
|
directly_related = isinstance(actual, type(expected)) or isinstance(
|
|
expected, type(actual)
|
|
)
|
|
if not directly_related:
|
|
self._inputs_not_supported()
|
|
|
|
if not allow_subclasses and type(actual) is not type(expected):
|
|
self._inputs_not_supported()
|
|
|
|
actual, expected = (
|
|
self._to_tensor(input) for input in (actual, expected)
|
|
)
|
|
return actual, expected
|
|
|
|
def _to_tensor(self, tensor_like: Any) -> paddle.Tensor:
|
|
if isinstance(tensor_like, paddle.Tensor):
|
|
return tensor_like
|
|
|
|
try:
|
|
return paddle.to_tensor(tensor_like)
|
|
except Exception:
|
|
self._inputs_not_supported()
|
|
|
|
def compare(self) -> None:
|
|
actual, expected = self.actual, self.expected
|
|
|
|
self._compare_attributes(actual, expected)
|
|
|
|
actual, expected = self._equalize_attributes(actual, expected)
|
|
self._compare_values(actual, expected)
|
|
|
|
def _compare_attributes(
|
|
self,
|
|
actual: paddle.Tensor,
|
|
expected: paddle.Tensor,
|
|
) -> None:
|
|
def raise_mismatch_error(
|
|
attribute_name: str, actual_value: Any, expected_value: Any
|
|
) -> NoReturn:
|
|
self._fail(
|
|
AssertionError,
|
|
f"The values for attribute '{attribute_name}' do not match: {actual_value} != {expected_value}.",
|
|
)
|
|
|
|
if actual.shape != expected.shape:
|
|
raise_mismatch_error("shape", actual.shape, expected.shape)
|
|
|
|
if self.check_device and actual.place != expected.place:
|
|
raise_mismatch_error("device", actual.place, expected.place)
|
|
|
|
if self.check_dtype and actual.dtype != expected.dtype:
|
|
raise_mismatch_error("dtype", actual.dtype, expected.dtype)
|
|
|
|
def _equalize_attributes(
|
|
self, actual: paddle.Tensor, expected: paddle.Tensor
|
|
) -> tuple[paddle.Tensor, paddle.Tensor]:
|
|
if str(actual.place) != str(expected.place):
|
|
actual = actual.cpu()
|
|
expected = expected.cpu()
|
|
|
|
if actual.dtype != expected.dtype:
|
|
actual_dtype = actual.dtype
|
|
expected_dtype = expected.dtype
|
|
# For uint64, this is not sound in general, which is why promote_types doesn't
|
|
# allow it, but for easy testing, we're unlikely to get confused
|
|
# by large uint64 overflowing into negative int64
|
|
if actual_dtype in [paddle.uint64, paddle.uint32, paddle.uint16]:
|
|
actual_dtype = paddle.int64
|
|
if expected_dtype in [paddle.uint64, paddle.uint32, paddle.uint16]:
|
|
expected_dtype = paddle.int64
|
|
dtype = promote_types(actual_dtype, expected_dtype)
|
|
actual = actual.astype(dtype)
|
|
expected = expected.astype(dtype)
|
|
|
|
return actual, expected
|
|
|
|
def _compare_values(
|
|
self, actual: paddle.Tensor, expected: paddle.Tensor
|
|
) -> None:
|
|
self._compare_regular_values_close(
|
|
actual,
|
|
expected,
|
|
rtol=self.rtol,
|
|
atol=self.atol,
|
|
equal_nan=self.equal_nan,
|
|
)
|
|
|
|
def _compare_regular_values_close(
|
|
self,
|
|
actual: paddle.Tensor,
|
|
expected: paddle.Tensor,
|
|
*,
|
|
rtol: float,
|
|
atol: float,
|
|
equal_nan: bool,
|
|
identifier: str | Callable[[str], str] | None = None,
|
|
) -> None:
|
|
"""Checks if the values of two tensors are close up to a desired tolerance."""
|
|
matches = paddle.isclose(
|
|
actual, expected, rtol=rtol, atol=atol, equal_nan=equal_nan
|
|
)
|
|
|
|
if paddle.all(matches):
|
|
return
|
|
|
|
if actual.shape == []:
|
|
msg = make_scalar_mismatch_msg(
|
|
actual.item(),
|
|
expected.item(),
|
|
rtol=rtol,
|
|
atol=atol,
|
|
identifier=identifier,
|
|
)
|
|
else:
|
|
msg = make_tensor_mismatch_msg(
|
|
actual,
|
|
expected,
|
|
matches,
|
|
rtol=rtol,
|
|
atol=atol,
|
|
identifier=identifier,
|
|
)
|
|
self._fail(AssertionError, msg)
|
|
|
|
def extra_repr(self) -> Sequence[str]:
|
|
return (
|
|
"rtol",
|
|
"atol",
|
|
"equal_nan",
|
|
"check_device",
|
|
"check_dtype",
|
|
)
|
|
|
|
|
|
def originate_pairs(
|
|
actual: Any,
|
|
expected: Any,
|
|
*,
|
|
pair_types: Sequence[type[Pair]],
|
|
sequence_types: tuple[type, ...] = (collections.abc.Sequence,),
|
|
mapping_types: tuple[type, ...] = (collections.abc.Mapping,),
|
|
id: tuple[Any, ...] = (),
|
|
**options: Any,
|
|
) -> list[Pair]:
|
|
if (
|
|
isinstance(actual, sequence_types)
|
|
and not isinstance(actual, str)
|
|
and isinstance(expected, sequence_types)
|
|
and not isinstance(expected, str)
|
|
):
|
|
actual_len = len(actual)
|
|
expected_len = len(expected)
|
|
if actual_len != expected_len:
|
|
raise ErrorMeta(
|
|
AssertionError,
|
|
f"The length of the sequences mismatch: {actual_len} != {expected_len}",
|
|
id=id,
|
|
)
|
|
|
|
pairs = []
|
|
for idx in range(actual_len):
|
|
pairs.extend(
|
|
originate_pairs(
|
|
actual[idx],
|
|
expected[idx],
|
|
pair_types=pair_types,
|
|
sequence_types=sequence_types,
|
|
mapping_types=mapping_types,
|
|
id=(*id, idx),
|
|
**options,
|
|
)
|
|
)
|
|
return pairs
|
|
|
|
elif isinstance(actual, mapping_types) and isinstance(
|
|
expected, mapping_types
|
|
):
|
|
actual_keys = set(actual.keys())
|
|
expected_keys = set(expected.keys())
|
|
if actual_keys != expected_keys:
|
|
missing_keys = expected_keys - actual_keys
|
|
additional_keys = actual_keys - expected_keys
|
|
raise ErrorMeta(
|
|
AssertionError,
|
|
(
|
|
f"The keys of the mappings do not match:\n"
|
|
f"Missing keys in the actual mapping: {sorted(missing_keys)}\n"
|
|
f"Additional keys in the actual mapping: {sorted(additional_keys)}"
|
|
),
|
|
id=id,
|
|
)
|
|
|
|
keys: Collection = actual_keys
|
|
with contextlib.suppress(Exception):
|
|
keys = sorted(keys)
|
|
|
|
pairs = []
|
|
for key in keys:
|
|
pairs.extend(
|
|
originate_pairs(
|
|
actual[key],
|
|
expected[key],
|
|
pair_types=pair_types,
|
|
sequence_types=sequence_types,
|
|
mapping_types=mapping_types,
|
|
id=(*id, key),
|
|
**options,
|
|
)
|
|
)
|
|
return pairs
|
|
|
|
else:
|
|
for pair_type in pair_types:
|
|
try:
|
|
return [pair_type(actual, expected, id=id, **options)]
|
|
except UnsupportedInputs:
|
|
continue
|
|
except ErrorMeta:
|
|
raise
|
|
except Exception as error:
|
|
raise RuntimeError(
|
|
f"Originating a {pair_type.__name__}() at item {''.join(str([item]) for item in id)} with\n\n"
|
|
f"{type(actual).__name__}(): {actual}\n\n"
|
|
f"and\n\n"
|
|
f"{type(expected).__name__}(): {expected}\n\n"
|
|
f"resulted in the unexpected exception above. "
|
|
) from error
|
|
else:
|
|
raise ErrorMeta(
|
|
TypeError,
|
|
f"No comparison pair was able to handle inputs of type {type(actual)} and {type(expected)}.",
|
|
id=id,
|
|
)
|
|
|
|
|
|
def not_close_error_metas(
|
|
actual: Any,
|
|
expected: Any,
|
|
*,
|
|
pair_types: Sequence[type[Pair]] = (ObjectPair,),
|
|
sequence_types: tuple[type, ...] = (collections.abc.Sequence,),
|
|
mapping_types: tuple[type, ...] = (collections.abc.Mapping,),
|
|
**options: Any,
|
|
) -> list[ErrorMeta]:
|
|
# Hide this function from `pytest`'s traceback
|
|
__tracebackhide__ = True
|
|
|
|
try:
|
|
pairs = originate_pairs(
|
|
actual,
|
|
expected,
|
|
pair_types=pair_types,
|
|
sequence_types=sequence_types,
|
|
mapping_types=mapping_types,
|
|
**options,
|
|
)
|
|
except ErrorMeta as error_meta:
|
|
raise error_meta.to_error() from None
|
|
|
|
error_metas: list[ErrorMeta] = []
|
|
for pair in pairs:
|
|
try:
|
|
pair.compare()
|
|
except ErrorMeta as error_meta:
|
|
error_metas.append(error_meta)
|
|
except Exception as error:
|
|
raise RuntimeError(
|
|
f"Comparing\n\n"
|
|
f"{pair}\n\n"
|
|
f"resulted in the unexpected exception above."
|
|
) from error
|
|
|
|
error_metas = [error_metas]
|
|
return error_metas.pop()
|
|
|
|
|
|
def assert_close(
|
|
actual: Any,
|
|
expected: Any,
|
|
*,
|
|
allow_subclasses: bool = True,
|
|
rtol: float | None = None,
|
|
atol: float | None = None,
|
|
equal_nan: bool = False,
|
|
check_device: bool = True,
|
|
check_dtype: bool = True,
|
|
msg: str | Callable[[str], str] | None = None,
|
|
) -> None:
|
|
r"""
|
|
Asserts that ``actual`` and ``expected`` are close.
|
|
|
|
If ``actual`` and ``expected`` are real-valued, and finite, they are considered close if
|
|
|
|
.. math::
|
|
|
|
\lvert \text{actual} - \text{expected} \rvert \le \texttt{atol} + \texttt{rtol} \cdot \lvert \text{expected} \rvert
|
|
|
|
Non-finite values (``-inf`` and ``inf``) are only considered close if and only if they are equal. ``NaN``'s are
|
|
only considered equal to each other if ``equal_nan`` is ``True``.
|
|
|
|
In addition, they are only considered close if they have the same
|
|
|
|
- :attr:`~paddle.Tensor.place` (if ``check_device`` is ``True``),
|
|
- ``dtype`` (if ``check_dtype`` is ``True``),
|
|
|
|
In static graph mode, only the check_dtype attribute verification will be performed.
|
|
|
|
``actual`` and ``expected`` can be :class:`~paddle.Tensor`'s or any tensor-or-scalar-likes from which
|
|
:class:`paddle.Tensor`'s can be constructed with :func:`paddle.to_tensor`. Except for Python scalars the input types
|
|
have to be directly related. In addition, ``actual`` and ``expected`` can be :class:`~collections.abc.Sequence`'s
|
|
or :class:`~collections.abc.Mapping`'s in which case they are considered close if their structure matches and all
|
|
their elements are considered close according to the above definition.
|
|
|
|
.. note::
|
|
|
|
Python scalars are an exception to the type relation requirement, because their :func:`type`, i.e.
|
|
:class:`int`, :class:`float`, and :class:`complex`, is equivalent to the ``dtype`` of a tensor-like. Thus,
|
|
Python scalars of different types can be checked, but require ``check_dtype=False``.
|
|
|
|
Args:
|
|
actual (Any): Actual input.
|
|
expected (Any): Expected input.
|
|
allow_subclasses (bool): If ``True`` (default) and except for Python scalars, inputs of directly related types
|
|
are allowed. Otherwise type equality is required.
|
|
rtol (float, optional): Relative tolerance. If specified ``atol`` must also be specified. If omitted, default
|
|
values based on the :attr:`~paddle.Tensor.dtype` are selected with the below table.
|
|
atol (float, optional): Absolute tolerance. If specified ``rtol`` must also be specified. If omitted, default
|
|
values based on the :attr:`~paddle.Tensor.dtype` are selected with the below table.
|
|
equal_nan (bool|str, optional): If ``True``, two ``NaN`` values will be considered equal.
|
|
check_device (bool): If ``True`` (default), asserts that corresponding tensors are on the same
|
|
:attr:`~paddle.Tensor.place`. If this check is disabled, tensors on different
|
|
:attr:`~paddle.Tensor.place`'s are moved to the CPU before being compared.
|
|
check_dtype (bool): If ``True`` (default), asserts that corresponding tensors have the same ``dtype``. If this
|
|
check is disabled, tensors with different ``dtype``'s are promoted to a common ``dtype`` before being compared.
|
|
msg (str|Callable[[str], str], optional): Optional error message to use in case a failure occurs during
|
|
the comparison. Can also be passed as callable in which case it will be called with the generated message and
|
|
should return the new message.
|
|
|
|
The following table displays the default ``rtol`` and ``atol`` for different ``dtype``'s. In case of mismatching
|
|
``dtype``'s, the maximum of both tolerances is used.
|
|
|
|
+---------------------------+------------+----------+
|
|
| ``dtype`` | ``rtol`` | ``atol`` |
|
|
+===========================+============+==========+
|
|
| :attr:`~paddle.float16` | ``1e-3`` | ``1e-5`` |
|
|
+---------------------------+------------+----------+
|
|
| :attr:`~paddle.bfloat16` | ``1.6e-2`` | ``1e-5`` |
|
|
+---------------------------+------------+----------+
|
|
| :attr:`~paddle.float32` | ``1.3e-6`` | ``1e-5`` |
|
|
+---------------------------+------------+----------+
|
|
| :attr:`~paddle.float64` | ``1e-7`` | ``1e-7`` |
|
|
+---------------------------+------------+----------+
|
|
| :attr:`~paddle.complex64` | ``1.3e-6`` | ``1e-5`` |
|
|
+---------------------------+------------+----------+
|
|
| :attr:`~paddle.complex128`| ``1e-7`` | ``1e-7`` |
|
|
+---------------------------+------------+----------+
|
|
| other | ``0.0`` | ``0.0`` |
|
|
+---------------------------+------------+----------+
|
|
|
|
.. note::
|
|
|
|
This function is highly configurable with strict default settings. Users are encouraged
|
|
to :func:`~functools.partial` it to fit their use case.
|
|
|
|
Examples:
|
|
|
|
.. code-block:: pycon
|
|
|
|
>>> import paddle
|
|
>>> import numpy as np
|
|
>>> import functools
|
|
|
|
>>> # tensor to tensor comparison
|
|
>>> expected = paddle.to_tensor([1e0, 1e-1, 1e-2])
|
|
>>> actual = paddle.acos(paddle.cos(expected))
|
|
>>> paddle.testing.assert_close(actual, expected)
|
|
|
|
>>> # scalar to scalar comparison
|
|
>>> import math
|
|
>>> expected = math.sqrt(2.0)
|
|
>>> actual = 2.0 / math.sqrt(2.0)
|
|
>>> paddle.testing.assert_close(actual, expected)
|
|
|
|
>>> # numpy array to numpy array comparison
|
|
>>> expected = np.array([1e0, 1e-1, 1e-2])
|
|
>>> actual = np.arccos(np.cos(expected))
|
|
>>> paddle.testing.assert_close(actual, expected)
|
|
|
|
>>> # sequence to sequence comparison
|
|
>>> # The types of the sequences do not have to match. They only have to have the same
|
|
>>> # length and their elements have to match.
|
|
>>> expected = [paddle.to_tensor([1.0]), 2.0, np.array(3.0)]
|
|
>>> actual = tuple(expected)
|
|
>>> paddle.testing.assert_close(actual, expected)
|
|
|
|
>>> # mapping to mapping comparison
|
|
>>> from collections import OrderedDict
|
|
>>> foo = paddle.to_tensor(1.0)
|
|
>>> bar = 2.0
|
|
>>> baz = np.array(3.0)
|
|
>>> # The types and a possible ordering of mappings do not have to match. They only
|
|
>>> # have to have the same set of keys and their elements have to match.
|
|
>>> expected = OrderedDict([("foo", foo), ("bar", bar), ("baz", baz)])
|
|
>>> actual = {"baz": baz, "bar": bar, "foo": foo}
|
|
>>> paddle.testing.assert_close(actual, expected)
|
|
|
|
>>> # Customize the error message
|
|
>>> expected = paddle.to_tensor([1.0, 2.0, 3.0])
|
|
>>> actual = paddle.to_tensor([1.0, 4.0, 5.0])
|
|
>>> try:
|
|
... paddle.testing.assert_close(actual, expected, msg="Argh, the tensors are not close!")
|
|
... except AssertionError as e:
|
|
... print(e)
|
|
Argh, the tensors are not close!
|
|
|
|
>>> # Using functools to create strict equality check
|
|
>>> assert_equal = functools.partial(paddle.testing.assert_close, rtol=0, atol=0)
|
|
>>> try:
|
|
... assert_equal(1e-9, 1e-10)
|
|
... except AssertionError as e:
|
|
... print(e)
|
|
Scalars are not equal!
|
|
<BLANKLINE>
|
|
Expected 1e-10 but got 1e-09.
|
|
Absolute difference: 9.000000000000001e-10
|
|
Relative difference: 9.0
|
|
|
|
>>> # NaN check
|
|
>>> expected = paddle.to_tensor(float("Nan"))
|
|
>>> actual = expected.clone()
|
|
>>> # NaN != NaN by default, so this raises AssertionError
|
|
>>> try:
|
|
... paddle.testing.assert_close(actual, expected)
|
|
... except AssertionError as e:
|
|
... print("Assertion Failed")
|
|
Assertion Failed
|
|
>>> # Pass equal_nan=True to succeed
|
|
>>> paddle.testing.assert_close(actual, expected, equal_nan=True)
|
|
"""
|
|
# Hide this function from `pytest`'s traceback
|
|
__tracebackhide__ = True
|
|
|
|
error_metas = not_close_error_metas(
|
|
actual,
|
|
expected,
|
|
pair_types=(
|
|
NonePair,
|
|
BooleanPair,
|
|
NumberPair,
|
|
StaticPair,
|
|
TensorLikePair,
|
|
),
|
|
allow_subclasses=allow_subclasses,
|
|
rtol=rtol,
|
|
atol=atol,
|
|
equal_nan=equal_nan,
|
|
check_device=check_device,
|
|
check_dtype=check_dtype,
|
|
msg=msg,
|
|
)
|
|
|
|
if error_metas:
|
|
raise error_metas[0].to_error(msg)
|
|
|
|
|
|
def _assert(condition, message=""):
|
|
r"""
|
|
A wrapper around Python's assert which is symbolically traceable.
|
|
|
|
In dynamic graph mode, this function behaves like a regular Python assert.
|
|
In static graph mode, when the condition is a Tensor, it creates an Assert
|
|
op in the computation graph.
|
|
|
|
Args:
|
|
condition (bool or Tensor): The condition to assert. If a Tensor, it
|
|
must be a boolean scalar (numel=1).
|
|
message (str, optional): The error message to display when the assertion
|
|
fails. Default: "".
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> import paddle
|
|
>>> # Non-tensor condition
|
|
>>> paddle._assert(1 == 1, "This should pass")
|
|
|
|
>>> # Tensor condition
|
|
>>> x = paddle.to_tensor([True])
|
|
>>> paddle._assert(x, "Tensor assertion")
|
|
|
|
"""
|
|
from paddle.base.framework import Variable
|
|
from paddle.framework import in_dynamic_mode
|
|
|
|
if isinstance(condition, (paddle.Tensor, paddle.pir.Value, Variable)):
|
|
if in_dynamic_mode():
|
|
if not condition:
|
|
raise AssertionError(message)
|
|
else:
|
|
condition = paddle.cast(condition, "bool")
|
|
from paddle.static.nn.control_flow import Assert
|
|
|
|
return Assert(condition)
|
|
else:
|
|
if not condition:
|
|
raise AssertionError(message)
|
|
|
|
|
|
def assert_allclose(
|
|
actual: Any,
|
|
expected: Any,
|
|
rtol: float | None = None,
|
|
atol: float | None = None,
|
|
equal_nan: bool = True,
|
|
msg: str = "",
|
|
) -> None:
|
|
r"""
|
|
Asserts that ``actual`` and ``expected`` are close.
|
|
|
|
.. warning::
|
|
This API is deprecated. Please use ``paddle.testing.assert_allclose`` instead.
|
|
|
|
If ``actual`` and ``expected`` are real-valued, and finite, they are considered close if
|
|
|
|
.. math::
|
|
|
|
\lvert \text{actual} - \text{expected} \rvert \le \texttt{atol} + \texttt{rtol} \cdot \lvert \text{expected} \rvert
|
|
|
|
Non-finite values (``-inf`` and ``inf``) are only considered close if and only if they are equal.
|
|
``NaN``'s are only considered equal to each other if ``equal_nan`` is ``True``.
|
|
|
|
Args:
|
|
actual (Any): The actual value.
|
|
expected (Any): The expected value.
|
|
rtol (float|None, optional): Relative tolerance. If None, uses default tolerances.
|
|
Default: None.
|
|
atol (float|None, optional): Absolute tolerance. If None, uses default tolerances.
|
|
Default: None.
|
|
equal_nan (bool, optional): If True, NaN values are considered equal. Default: True.
|
|
msg (str, optional): Custom error message. Default: "".
|
|
|
|
Raises:
|
|
AssertionError: If ``actual`` and ``expected`` are not close.
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> import paddle
|
|
>>> paddle.testing.assert_allclose(paddle.to_tensor([1.0]), paddle.to_tensor([1.0]))
|
|
"""
|
|
if not isinstance(actual, paddle.Tensor):
|
|
actual = paddle.to_tensor(actual)
|
|
if not isinstance(expected, paddle.Tensor):
|
|
expected = paddle.to_tensor(expected, dtype=actual.dtype)
|
|
|
|
if rtol is None and atol is None:
|
|
rtol, atol = default_tolerances(
|
|
actual,
|
|
expected,
|
|
dtype_precisions={
|
|
paddle.float16: (1e-3, 1e-3),
|
|
paddle.float32: (1e-4, 1e-5),
|
|
paddle.float64: (1e-5, 1e-8),
|
|
},
|
|
)
|
|
|
|
assert_close(
|
|
actual,
|
|
expected,
|
|
rtol=rtol,
|
|
atol=atol,
|
|
equal_nan=equal_nan,
|
|
check_device=True,
|
|
check_dtype=False,
|
|
msg=msg or None,
|
|
)
|