diff --git a/include/dxc/Test/HlslTestUtils.h b/include/dxc/Test/HlslTestUtils.h index 5a84f5cfbc..f7b941a1bb 100644 --- a/include/dxc/Test/HlslTestUtils.h +++ b/include/dxc/Test/HlslTestUtils.h @@ -580,7 +580,7 @@ inline bool CompareDoubleULP( } inline bool CompareDoubleEpsilon(const double &Src, const double &Ref, - float Epsilon) { + double Epsilon) { if (Src == Ref) { return true; } diff --git a/tools/clang/unittests/HLSLExec/LongVectorTestData.h b/tools/clang/unittests/HLSLExec/LongVectorTestData.h index 24a4301299..1aaca6a467 100644 --- a/tools/clang/unittests/HLSLExec/LongVectorTestData.h +++ b/tools/clang/unittests/HLSLExec/LongVectorTestData.h @@ -12,6 +12,8 @@ #include #include +#include "dxc/Support/Global.h" + namespace LongVector { // A helper struct because C++ bools are 1 byte and HLSL bools are 4 bytes. @@ -118,6 +120,18 @@ struct HLSLHalf_t { // float. HLSLHalf_t(DirectX::PackedVector::HALF) = delete; + static double GetULP(HLSLHalf_t A) { + DXASSERT(!std::isnan(A) && !std::isinf(A), + "ULP of NaN or infinity is undefined"); + + HLSLHalf_t Next = A; + ++Next.Val; + + double NextD = Next; + double AD = A; + return NextD - AD; + } + static HLSLHalf_t FromHALF(DirectX::PackedVector::HALF Half) { HLSLHalf_t H; H.Val = Half; @@ -184,10 +198,6 @@ struct HLSLHalf_t { return FromHALF((DirectX::PackedVector::XMConvertFloatToHalf(A + B))); } - HLSLHalf_t &operator+=(const HLSLHalf_t &Other) { - return *this = *this + Other; - } - HLSLHalf_t operator-(const HLSLHalf_t &Other) const { const float A = DirectX::PackedVector::XMConvertHalfToFloat(Val); const float B = DirectX::PackedVector::XMConvertHalfToFloat(Other.Val); diff --git a/tools/clang/unittests/HLSLExec/LongVectors.cpp b/tools/clang/unittests/HLSLExec/LongVectors.cpp index 93a46d02fb..f80925740f 100644 --- a/tools/clang/unittests/HLSLExec/LongVectors.cpp +++ b/tools/clang/unittests/HLSLExec/LongVectors.cpp @@ -12,6 +12,7 @@ #include "HlslExecTestUtils.h" +#include #include #include #include @@ -175,25 +176,25 @@ enum class ValidationType { }; template -bool doValuesMatch(T A, T B, float Tolerance, ValidationType) { - if (Tolerance == 0.0f) +bool doValuesMatch(T A, T B, double Tolerance, ValidationType) { + if (Tolerance == 0.0) return A == B; T Diff = A > B ? A - B : B - A; return Diff <= Tolerance; } -bool doValuesMatch(HLSLBool_t A, HLSLBool_t B, float, ValidationType) { +bool doValuesMatch(HLSLBool_t A, HLSLBool_t B, double, ValidationType) { return A == B; } -bool doValuesMatch(HLSLHalf_t A, HLSLHalf_t B, float Tolerance, +bool doValuesMatch(HLSLHalf_t A, HLSLHalf_t B, double Tolerance, ValidationType ValidationType) { switch (ValidationType) { case ValidationType::Epsilon: - return CompareHalfEpsilon(A.Val, B.Val, Tolerance); + return CompareHalfEpsilon(A.Val, B.Val, static_cast(Tolerance)); case ValidationType::Ulp: - return CompareHalfULP(A.Val, B.Val, Tolerance); + return CompareHalfULP(A.Val, B.Val, static_cast(Tolerance)); default: hlsl_test::LogErrorFmt( L"Invalid ValidationType. Expecting Epsilon or ULP."); @@ -201,11 +202,11 @@ bool doValuesMatch(HLSLHalf_t A, HLSLHalf_t B, float Tolerance, } } -bool doValuesMatch(float A, float B, float Tolerance, +bool doValuesMatch(float A, float B, double Tolerance, ValidationType ValidationType) { switch (ValidationType) { case ValidationType::Epsilon: - return CompareFloatEpsilon(A, B, Tolerance); + return CompareFloatEpsilon(A, B, static_cast(Tolerance)); case ValidationType::Ulp: { // Tolerance is in ULPs. Convert to int for the comparison. const int IntTolerance = static_cast(Tolerance); @@ -218,7 +219,7 @@ bool doValuesMatch(float A, float B, float Tolerance, } } -bool doValuesMatch(double A, double B, float Tolerance, +bool doValuesMatch(double A, double B, double Tolerance, ValidationType ValidationType) { switch (ValidationType) { case ValidationType::Epsilon: @@ -237,7 +238,7 @@ bool doValuesMatch(double A, double B, float Tolerance, template bool doVectorsMatch(const std::vector &ActualValues, - const std::vector &ExpectedValues, float Tolerance, + const std::vector &ExpectedValues, double Tolerance, ValidationType ValidationType, bool VerboseLogging) { DXASSERT( @@ -247,6 +248,11 @@ bool doVectorsMatch(const std::vector &ActualValues, if (VerboseLogging) { logLongVector(ActualValues, L"ActualValues"); logLongVector(ExpectedValues, L"ExpectedValues"); + + hlsl_test::LogCommentFmt( + L"ValidationType: %s, Tolerance: %17g", + ValidationType == ValidationType::Epsilon ? L"Epsilon" : L"ULP", + Tolerance); } // Stash mismatched indexes for easy failure logging later @@ -534,14 +540,14 @@ InputSets buildTestInputs(size_t VectorSize, const InputSet OpInputSets[3], } struct ValidationConfig { - float Tolerance = 0.0f; + double Tolerance = 0.0; ValidationType Type = ValidationType::Epsilon; - static ValidationConfig Epsilon(float Tolerance) { + static ValidationConfig Epsilon(double Tolerance) { return ValidationConfig{Tolerance, ValidationType::Epsilon}; } - static ValidationConfig Ulp(float Tolerance) { + static ValidationConfig Ulp(double Tolerance) { return ValidationConfig{Tolerance, ValidationType::Ulp}; } }; @@ -593,7 +599,8 @@ template struct DefaultValidation { } }; -// Strict Validation - require exact matches for all types +// Strict Validation - Defaults to exact matches. +// Tolerance can be set to a non-zero value to allow for a wider range. struct StrictValidation { ValidationConfig ValidationConfig; }; @@ -935,7 +942,7 @@ struct Op : StrictValidation {}; // values. template <> struct ExpectedBuilder { static std::vector - buildExpected(Op, + buildExpected(Op &, const InputSets &Inputs) { DXASSERT_NOMSG(Inputs.size() == 1); @@ -1009,7 +1016,7 @@ DEFAULT_OP_1(OpType::Log2, (std::log2(A))); template <> struct Op : DefaultValidation {}; template <> struct ExpectedBuilder { - static std::vector buildExpected(Op, + static std::vector buildExpected(Op &, const InputSets &Inputs) { DXASSERT_NOMSG(Inputs.size() == 1); @@ -1079,7 +1086,7 @@ OP_3(OpType::Select, StrictValidation, (static_cast(A) ? B : C)); #define REDUCTION_OP(OP, STDFUNC) \ template struct Op : StrictValidation {}; \ template struct ExpectedBuilder { \ - static std::vector buildExpected(Op, \ + static std::vector buildExpected(Op &, \ const InputSets &Inputs) { \ const bool Res = STDFUNC(Inputs[0].begin(), Inputs[0].end(), \ [](T A) { return A != static_cast(0); }); \ @@ -1097,22 +1104,97 @@ REDUCTION_OP(OpType::All_Zero, (std::all_of)); #undef REDUCTION_OP -template struct Op : DefaultValidation {}; +template struct Op : StrictValidation {}; template struct ExpectedBuilder { - static std::vector buildExpected(Op, + // For Dot, buildExpected is a special case: it also computes an absolute + // epsilon for validation because Dot is a compound operation. Expected value + // is computed by multiplying and accumulating in fp64 for higher precision. + // Absolute epsilon is computed by reordering the accumulation into a + // worst-case sequence, then summing the per-step epsilons to produce a + // conservative error tolerance for the entire Dot operation. + static std::vector buildExpected(Op &Op, const InputSets &Inputs) { - T DotProduct = T(); - for (size_t I = 0; I < Inputs[0].size(); ++I) { - DotProduct += Inputs[0][I] * Inputs[1][I]; + std::vector PositiveProducts; + std::vector NegativeProducts; + + const size_t VectorSize = Inputs[0].size(); + + // Floating point ops have a tolerance of 0.5 ULPs per operation as per the + // DX spec. + const double ULPTolerance = 0.5; + + // Accumulate in fp64 to improve precision. + double DotProduct = 0.0; // computed reference result + double AbsoluteEpsilon = 0.0; // computed tolerance + for (size_t I = 0; I < VectorSize; ++I) { + double Product = Inputs[0][I] * Inputs[1][I]; + AbsoluteEpsilon += computeAbsoluteEpsilon(Product, ULPTolerance); + + DotProduct += Product; + + if (Product >= 0.0) + PositiveProducts.push_back(Product); + else + NegativeProducts.push_back(Product); } + // Sort each by magnitude so that we can accumulate them in worst case + // order. + std::sort(PositiveProducts.begin(), PositiveProducts.end(), + std::greater()); + std::sort(NegativeProducts.begin(), NegativeProducts.end()); + + // Helper to sum the products and compute/add to the running absolute + // epsilon total. + auto SumProducts = [&AbsoluteEpsilon, + ULPTolerance](const std::vector &Values) { + double Sum = Values.empty() ? 0.0 : Values[0]; + for (size_t I = 1; I < Values.size(); ++I) { + Sum += Values[I]; + AbsoluteEpsilon += computeAbsoluteEpsilon(Sum, ULPTolerance); + } + return Sum; + }; + + // Accumulate products in the worst case order while computing the absolute + // epsilon error for each intermediate step. And accumulate that error. + const double SumPos = SumProducts(PositiveProducts); + const double SumNeg = SumProducts(NegativeProducts); + + if (!PositiveProducts.empty() && !NegativeProducts.empty()) + AbsoluteEpsilon += + computeAbsoluteEpsilon((SumPos + SumNeg), ULPTolerance); + + Op.ValidationConfig = ValidationConfig::Epsilon(AbsoluteEpsilon); + std::vector Expected; - Expected.push_back(DotProduct); + Expected.push_back(static_cast(DotProduct)); return Expected; } }; +template +static double computeAbsoluteEpsilon(double A, double ULPTolerance) { + DXASSERT((!isinf(A) && !isnan(A)), + "Input values should not produce inf or nan results"); + + // ULP is a positive value by definition. So, working with abs(A) simplifies + // our logic for computing ULP in the first place. + A = std::abs(A); + + double ULP = 0.0; + + if constexpr (std::is_same_v) + ULP = HLSLHalf_t::GetULP(A); + else + ULP = + std::nextafter(static_cast(A), std::numeric_limits::infinity()) - + static_cast(A); + + return ULP * ULPTolerance; +} + template struct Op : DefaultValidation {}; template struct ExpectedBuilder {