433 lines
16 KiB
Python
433 lines
16 KiB
Python
#
|
|
# SPDX-FileCopyrightText: Copyright (c) 1993-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# 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.
|
|
#
|
|
import numpy as np
|
|
import pytest
|
|
from polygraphy import util
|
|
from polygraphy.comparator import CompareFunc, IterationResult
|
|
from polygraphy.datatype import DataType
|
|
from polygraphy.exception import PolygraphyException
|
|
from polygraphy.logger import G_LOGGER
|
|
|
|
build_torch = lambda a, **kwargs: util.array.to_torch(np.array(a, **kwargs))
|
|
|
|
|
|
@pytest.mark.parametrize("array_type", [np.array, build_torch], ids=["numpy", "torch"])
|
|
class TestSimpleCompareFunc:
|
|
@pytest.mark.parametrize(
|
|
"values0, values1, dtype, expected_max_absdiff, expected_max_reldiff",
|
|
[
|
|
# Low precision arrays should be casted to higher precisions to avoid overflows/underflows.
|
|
([0], [1], DataType.UINT8, 1, 1.0),
|
|
([1], [0], DataType.UINT8, 1, np.inf),
|
|
([0], [1], DataType.UINT16, 1, 1.0),
|
|
([1], [0], DataType.UINT16, 1, np.inf),
|
|
([0], [1], DataType.UINT32, 1, 1.0),
|
|
([1], [0], DataType.UINT32, 1, np.inf),
|
|
([25], [30], DataType.INT8, 5, 5.0 / 30.0),
|
|
(
|
|
[25],
|
|
[30],
|
|
DataType.FLOAT16,
|
|
5,
|
|
np.array([5.0], dtype=np.float32) / np.array([30.0], dtype=np.float32),
|
|
),
|
|
([1], [0], DataType.FLOAT16, 1, 1 / np.finfo(float).eps),
|
|
],
|
|
)
|
|
def test_comparison(
|
|
self,
|
|
values0,
|
|
values1,
|
|
dtype,
|
|
expected_max_absdiff,
|
|
expected_max_reldiff,
|
|
array_type,
|
|
):
|
|
if array_type != np.array:
|
|
try:
|
|
DataType.to_dtype(dtype, "torch")
|
|
except:
|
|
pytest.skip(f"Cannot convert {dtype} to torch")
|
|
iter_result0 = IterationResult(
|
|
outputs={"output": array_type(values0, dtype=dtype.numpy())}
|
|
)
|
|
iter_result1 = IterationResult(
|
|
outputs={"output": array_type(values1, dtype=dtype.numpy())}
|
|
)
|
|
|
|
compare_func = CompareFunc.simple()
|
|
acc = compare_func(iter_result0, iter_result1)
|
|
|
|
comp_result = acc["output"]
|
|
assert np.isclose(comp_result.max_absdiff, expected_max_absdiff)
|
|
assert np.isclose(comp_result.max_absdiff, comp_result.mean_absdiff)
|
|
assert np.isclose(comp_result.max_absdiff, comp_result.median_absdiff)
|
|
|
|
assert np.isclose(comp_result.max_reldiff, expected_max_reldiff)
|
|
assert np.isclose(comp_result.max_reldiff, comp_result.mean_reldiff)
|
|
assert np.isclose(comp_result.max_reldiff, comp_result.median_reldiff)
|
|
|
|
@pytest.mark.parametrize(
|
|
"values0, values1, dtype, quantile, expected_abs_quantile, expected_rel_quantile",
|
|
[
|
|
([0, 0.1, 0.5, 0.75, 1], [2, 2, 2, 2, 2], DataType.FLOAT16, 0.5, 1.5, 0.75),
|
|
(
|
|
[0, 0.1, 0.5, 0.75, 1],
|
|
[2, 2, 2, 2, 2],
|
|
DataType.FLOAT16,
|
|
0.75,
|
|
1.9,
|
|
0.95,
|
|
),
|
|
(
|
|
[0.2, 0.2, 0.125, 0.11],
|
|
[0.1, 0.1, 0.1, 0.1],
|
|
DataType.FLOAT16,
|
|
0.5,
|
|
0.0625,
|
|
0.625,
|
|
),
|
|
([0, 1, 2, 3, 4], [2, 2, 2, 2, 2], DataType.UINT8, 0.5, 1, 0.5),
|
|
([0, 1, 2, 3, 4], [2, 2, 2, 2, 2], DataType.UINT8, 0.75, 2, 1),
|
|
],
|
|
)
|
|
def test_quantile(
|
|
self,
|
|
values0,
|
|
values1,
|
|
dtype,
|
|
quantile,
|
|
expected_abs_quantile,
|
|
expected_rel_quantile,
|
|
array_type,
|
|
):
|
|
if array_type != np.array:
|
|
try:
|
|
DataType.to_dtype(dtype, "torch")
|
|
except:
|
|
pytest.skip(f"Cannot convert {dtype} to torch")
|
|
iter_result0 = IterationResult(
|
|
outputs={"output": array_type(values0, dtype=dtype.numpy())}
|
|
)
|
|
iter_result1 = IterationResult(
|
|
outputs={"output": array_type(values1, dtype=dtype.numpy())}
|
|
)
|
|
|
|
compare_func = CompareFunc.simple(
|
|
check_error_stat="quantile", error_quantile=quantile
|
|
)
|
|
acc = compare_func(iter_result0, iter_result1)
|
|
|
|
comp_result = acc["output"]
|
|
assert np.isclose(
|
|
comp_result.quantile_absdiff, expected_abs_quantile, atol=1e-4, rtol=1e-4
|
|
)
|
|
assert comp_result.quantile_absdiff <= comp_result.max_absdiff
|
|
assert (quantile >= 0.5) == (
|
|
comp_result.quantile_absdiff >= comp_result.median_absdiff
|
|
)
|
|
|
|
assert np.isclose(
|
|
comp_result.quantile_reldiff, expected_rel_quantile, atol=1e-4, rtol=1e-4
|
|
)
|
|
assert comp_result.quantile_reldiff <= comp_result.max_reldiff
|
|
assert (quantile >= 0.5) == (
|
|
comp_result.quantile_reldiff >= comp_result.median_reldiff
|
|
)
|
|
|
|
def test_can_compare_bool(self, array_type):
|
|
iter_result0 = IterationResult(
|
|
outputs={"output": array_type(np.zeros((4, 4), dtype=bool))}
|
|
)
|
|
iter_result1 = IterationResult(
|
|
outputs={"output": array_type(np.ones((4, 4), dtype=bool))}
|
|
)
|
|
|
|
compare_func = CompareFunc.simple()
|
|
acc = compare_func(iter_result0, iter_result1)
|
|
|
|
assert not acc["output"]
|
|
|
|
@pytest.mark.parametrize("mode", ["abs", "rel"])
|
|
def test_per_output_tol(self, mode, array_type):
|
|
OUT0_NAME = "output0"
|
|
OUT1_NAME = "output1"
|
|
OUT_VALS = array_type(np.ones((4, 4)))
|
|
|
|
iter_result0 = IterationResult(
|
|
outputs={OUT0_NAME: OUT_VALS, OUT1_NAME: OUT_VALS}
|
|
)
|
|
iter_result1 = IterationResult(
|
|
outputs={OUT0_NAME: OUT_VALS, OUT1_NAME: OUT_VALS + 1}
|
|
)
|
|
|
|
# With default tolerances, out1 is wrong for the second result.
|
|
compare_func = CompareFunc.simple()
|
|
acc = compare_func(iter_result0, iter_result1)
|
|
assert bool(acc[OUT0_NAME])
|
|
assert not bool(acc[OUT1_NAME])
|
|
|
|
# But with custom tolerances, it should pass.
|
|
tols = {
|
|
OUT0_NAME: 0.0,
|
|
OUT1_NAME: 1.0,
|
|
}
|
|
|
|
if mode == "abs":
|
|
compare_func = CompareFunc.simple(atol=tols)
|
|
else:
|
|
compare_func = CompareFunc.simple(rtol=tols)
|
|
|
|
acc = compare_func(iter_result0, iter_result1)
|
|
assert bool(acc[OUT0_NAME])
|
|
assert bool(acc[OUT1_NAME])
|
|
|
|
@pytest.mark.parametrize("mode", ["abs", "rel"])
|
|
def test_per_output_tol_fallback(self, mode, array_type):
|
|
OUT0_NAME = "output0"
|
|
OUT1_NAME = "output1"
|
|
OUT_VALS = array_type(np.ones((4, 4)))
|
|
|
|
iter_result0 = IterationResult(
|
|
outputs={OUT0_NAME: OUT_VALS + 1, OUT1_NAME: OUT_VALS}
|
|
)
|
|
iter_result1 = IterationResult(
|
|
outputs={OUT0_NAME: OUT_VALS, OUT1_NAME: OUT_VALS + 1}
|
|
)
|
|
|
|
acc = CompareFunc.simple()(iter_result0, iter_result1)
|
|
assert not bool(acc[OUT0_NAME])
|
|
assert not bool(acc[OUT1_NAME])
|
|
|
|
# Do not specify tolerance for OUT0_NAME - it should fail with fallback tolerance
|
|
tols = {
|
|
OUT1_NAME: 1.0,
|
|
}
|
|
|
|
if mode == "abs":
|
|
compare_func = CompareFunc.simple(atol=tols)
|
|
else:
|
|
compare_func = CompareFunc.simple(rtol=tols)
|
|
|
|
acc = compare_func(iter_result0, iter_result1)
|
|
assert not bool(acc[OUT0_NAME])
|
|
assert bool(acc[OUT1_NAME])
|
|
|
|
@pytest.mark.parametrize("mode", ["abs", "rel"])
|
|
def test_default_tol_in_map(self, mode, array_type):
|
|
# "" can be used to indicate a global tolerance
|
|
OUT0_NAME = "output0"
|
|
OUT_VALS = array_type(np.ones((4, 4)))
|
|
|
|
iter_result0 = IterationResult(outputs={OUT0_NAME: OUT_VALS})
|
|
iter_result1 = IterationResult(outputs={OUT0_NAME: OUT_VALS + 1})
|
|
|
|
tols = {
|
|
"": 1.0,
|
|
}
|
|
|
|
if mode == "abs":
|
|
compare_func = CompareFunc.simple(atol=tols)
|
|
else:
|
|
compare_func = CompareFunc.simple(rtol=tols)
|
|
|
|
acc = compare_func(iter_result0, iter_result1)
|
|
assert bool(acc[OUT0_NAME])
|
|
|
|
@pytest.mark.parametrize(
|
|
"shape",
|
|
[
|
|
tuple(),
|
|
(0, 2, 1, 2),
|
|
(1,),
|
|
(2, 2, 2, 2),
|
|
],
|
|
)
|
|
def test_non_matching_outputs(self, shape, array_type):
|
|
iter_result0 = IterationResult(
|
|
outputs={"output": array_type(np.zeros(shape, dtype=np.float32))}
|
|
)
|
|
iter_result1 = IterationResult(
|
|
outputs={"output": array_type(np.ones(shape, dtype=np.float32))}
|
|
)
|
|
|
|
compare_func = CompareFunc.simple()
|
|
|
|
with G_LOGGER.verbosity(G_LOGGER.ULTRA_VERBOSE):
|
|
acc = compare_func(iter_result0, iter_result1)
|
|
|
|
assert util.is_empty_shape(shape) or not acc["output"]
|
|
|
|
@pytest.mark.parametrize("check_error_stat", ["max", "median", "mean", "elemwise"])
|
|
@pytest.mark.parametrize(
|
|
"func",
|
|
[
|
|
np.zeros,
|
|
np.ones,
|
|
],
|
|
)
|
|
def test_check_error_stat(self, func, check_error_stat, array_type):
|
|
iter_result0 = IterationResult(
|
|
outputs={"output": array_type(func((100,), dtype=np.float32))}
|
|
)
|
|
iter_result1 = IterationResult(
|
|
outputs={"output": array_type(func((100,), dtype=np.float32))}
|
|
)
|
|
|
|
iter_result0["output"][0] += 100
|
|
|
|
# Even though the max diff is 100, atol=1 should cause this to pass since we're checking
|
|
# against the mean error.
|
|
compare_func = CompareFunc.simple(check_error_stat=check_error_stat, atol=1)
|
|
|
|
if check_error_stat in ["max", "elemwise"]:
|
|
assert not compare_func(iter_result0, iter_result1)["output"]
|
|
else:
|
|
assert compare_func(iter_result0, iter_result1)["output"]
|
|
|
|
@pytest.mark.parametrize("check_error_stat", ["max", "median", "mean", "elemwise"])
|
|
def test_atol_rtol_either_pass(self, check_error_stat, array_type):
|
|
# If either rtol/atol is sufficient, the compare_func should pass
|
|
res0 = IterationResult(outputs={"output": array_type([1, 2], dtype=np.float32)})
|
|
res1 = IterationResult(
|
|
outputs={"output": array_type((1.25, 2.5), dtype=np.float32)}
|
|
)
|
|
|
|
assert not CompareFunc.simple(check_error_stat=check_error_stat)(res0, res1)[
|
|
"output"
|
|
]
|
|
|
|
assert CompareFunc.simple(check_error_stat=check_error_stat, rtol=0.25)(
|
|
res0, res1
|
|
)["output"]
|
|
assert CompareFunc.simple(check_error_stat=check_error_stat, atol=0.5)(
|
|
res0, res1
|
|
)["output"]
|
|
|
|
def test_atol_rtol_combined_pass(self, array_type):
|
|
# We should also be able to mix them - i.e. rtol might enough for some, atol for others.
|
|
# If they cover the entire output range, it should pass.
|
|
res0 = IterationResult(
|
|
outputs={"output": array_type([0, 1, 2, 3], dtype=np.float32)}
|
|
)
|
|
res1 = IterationResult(
|
|
outputs={"output": array_type((0.15, 1.25, 2.5, 3.75), dtype=np.float32)}
|
|
)
|
|
|
|
assert not CompareFunc.simple()(res0, res1)["output"]
|
|
|
|
assert not CompareFunc.simple(atol=0.3)(res0, res1)["output"]
|
|
assert not CompareFunc.simple(rtol=0.25)(res0, res1)["output"]
|
|
|
|
assert CompareFunc.simple(atol=0.3, rtol=0.25)(res0, res1)["output"]
|
|
|
|
@pytest.mark.parametrize(
|
|
"check_error_stat",
|
|
[
|
|
{"output0": "mean", "output1": "max"},
|
|
{"": "mean", "output1": "elemwise"},
|
|
{"output0": "mean"},
|
|
{"": "mean"},
|
|
],
|
|
)
|
|
def test_per_output_error_stat(self, check_error_stat, array_type):
|
|
# output0 will only pass when using check_error_stat=mean
|
|
res0 = IterationResult(
|
|
outputs={
|
|
"output0": array_type([0, 1, 2, 3], dtype=np.float32),
|
|
"output1": array_type([0, 1, 2, 3], dtype=np.float32),
|
|
}
|
|
)
|
|
res1 = IterationResult(
|
|
outputs={
|
|
"output0": array_type((0.15, 1.25, 2.5, 3.75), dtype=np.float32),
|
|
"output1": array_type((0, 1, 2, 3), dtype=np.float32),
|
|
}
|
|
)
|
|
|
|
atol = 0.4125
|
|
assert not CompareFunc.simple(atol=atol)(res0, res1)["output0"]
|
|
|
|
assert CompareFunc.simple(check_error_stat=check_error_stat, atol=atol)(
|
|
res0, res1
|
|
)["output0"]
|
|
assert CompareFunc.simple(check_error_stat=check_error_stat, atol=atol)(
|
|
res0, res1
|
|
)["output1"]
|
|
|
|
def test_invalid_error_stat(self, array_type):
|
|
res0 = IterationResult(
|
|
outputs={"output": array_type([0, 1, 2, 3], dtype=np.float32)}
|
|
)
|
|
res1 = IterationResult(
|
|
outputs={"output": array_type([0.15, 1.25, 2.5, 3.75], dtype=np.float32)}
|
|
)
|
|
|
|
with pytest.raises(PolygraphyException, match="Invalid choice"):
|
|
CompareFunc.simple(check_error_stat="invalid-stat")(res0, res1)
|
|
|
|
@pytest.mark.parametrize("check_error_stat", ["max", "median", "mean", "elemwise"])
|
|
@pytest.mark.parametrize("val0, val1", [(np.nan, 0.15), (0.15, np.nan)])
|
|
def test_nans_always_fail(self, check_error_stat, val0, val1, array_type):
|
|
res0 = IterationResult(outputs={"output": array_type([val0], dtype=np.float32)})
|
|
res1 = IterationResult(outputs={"output": array_type([val1], dtype=np.float32)})
|
|
|
|
assert not CompareFunc.simple(check_error_stat=check_error_stat)(res0, res1)[
|
|
"output"
|
|
]
|
|
|
|
@pytest.mark.parametrize("infinities_compare_equal", (False, True))
|
|
@pytest.mark.parametrize("val", (np.inf, -np.inf))
|
|
def test_infinities_compare_equal(self, infinities_compare_equal, val, array_type):
|
|
res0 = IterationResult(outputs={"output": array_type([val], dtype=np.float32)})
|
|
res1 = IterationResult(outputs={"output": array_type([val], dtype=np.float32)})
|
|
|
|
cf = CompareFunc.simple(infinities_compare_equal=infinities_compare_equal)
|
|
assert bool(cf(res0, res1)["output"]) == infinities_compare_equal
|
|
|
|
|
|
@pytest.mark.parametrize("array_type", [np.array, build_torch])
|
|
class TestIndicesCompareFunc:
|
|
@pytest.mark.parametrize(
|
|
"out0,out1,index_tolerance,expected",
|
|
[
|
|
([0, 1, 2, 3], [0, 1, 2, 3], 0, True),
|
|
# Check that dictionaries work for index tolerance
|
|
([0, 1, 2, 3], [0, 1, 2, 3], {"": 0}, True),
|
|
([0, 1, 2, 3], [0, 1, 2, 3], {"output": 0}, True),
|
|
([[0, 1], [0, 1], [0, 1]], [[0, 1], [0, 1], [0, 1]], 0, True),
|
|
([1, 0, 2, 3], [0, 1, 2, 3], 0, False),
|
|
([1, 0, 2, 3], [0, 1, 2, 3], 1, True),
|
|
([0, 1, 2, 3], [0, 1, 3, 2], 1, True),
|
|
# Last 'index_tolerance' indices should be ignored.
|
|
([1, 0, 2, 7], [0, 1, 2, 3], 0, False),
|
|
([1, 0, 2, 7], [0, 1, 2, 3], 1, True),
|
|
([[2, 3, 4], [5, 6, 9]], [[3, 2, 4], [5, 9, 6]], 0, False),
|
|
([[2, 3, 4], [5, 6, 9]], [[3, 2, 4], [5, 9, 6]], 1, True),
|
|
([0, 1, 2, 3, 4, 5, 6], [0, 3, 2, 1, 4, 5, 6], 0, False),
|
|
([0, 1, 2, 3, 4, 5, 6], [0, 3, 2, 1, 4, 5, 6], 2, True),
|
|
],
|
|
)
|
|
def test_index_tolerance(self, out0, out1, index_tolerance, expected, array_type):
|
|
res0 = IterationResult(outputs={"output": array_type(out0, dtype=np.int32)})
|
|
res1 = IterationResult(outputs={"output": array_type(out1, dtype=np.int32)})
|
|
|
|
assert (
|
|
CompareFunc.indices(index_tolerance=index_tolerance)(res0, res1)["output"]
|
|
== expected
|
|
)
|