Files
wehub-resource-sync c8a779b1bb
Docker Image CI / build-ubuntu2004 (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:55 +08:00

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
)