blob: db13a41935f0a7bed2d8c137464fb4d323f7b73d [file]
/*
* Copyright 2025 The ChromiumOS Authors
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "common/test_util/fp16_compare.h"
#include <gmock/gmock-matchers.h>
#include <algorithm>
#include <bit>
#include <cmath>
#include <cstdint>
#include <ostream>
#include <vector>
namespace {
// Converts an integer from the sign-and-magnitude representation to
// the biased representation. More precisely, let N be 2 to the
// power of (kBitCount - 1), an integer x is represented by the
// unsigned number x + N.
//
// For instance,
//
// -N + 1 (the most negative number representable using
// sign-and-magnitude) is represented by 1;
// 0 is represented by N; and
// N - 1 (the biggest number representable using
// sign-and-magnitude) is represented by 2N - 1.
//
// Read https://en.wikipedia.org/wiki/Signed_number_representations
// for more details on signed number representations.
uint32_t SignAndMagnitudeToBiased(uint32_t sam) {
constexpr uint32_t kSignBitMask = 1u << 31;
if (kSignBitMask & sam) {
// sam represents a negative number.
return ~sam + 1;
} else {
// sam represents a positive number.
return kSignBitMask | sam;
}
}
// Given two numbers in the sign-and-magnitude representation,
// returns the distance between them as an unsigned number.
uint32_t DistanceBetweenSignAndMagnitudeNumbers(uint32_t sam1, uint32_t sam2) {
uint32_t biased1 = SignAndMagnitudeToBiased(sam1);
uint32_t biased2 = SignAndMagnitudeToBiased(sam2);
return (biased1 >= biased2) ? (biased1 - biased2) : (biased2 - biased1);
}
// Returns true if and only if lhs is at most max_ulps ULP's away from rhs.
// In particular, this function:
//
// - returns true if both numbers are NAN.
// - returns false if exact one of numbers is NAN.
// - treats really large numbers as almost equal to infinity.
// - thinks +0.0 and -0.0 are 0 ULP's apart.
bool AlmostEquals(float lhs, float rhs, uint32_t max_ulps) {
if (std::isnan(lhs) || std::isnan(rhs)) {
return std::isnan(lhs) && std::isnan(rhs);
}
return DistanceBetweenSignAndMagnitudeNumbers(std::bit_cast<uint32_t>(lhs),
std::bit_cast<uint32_t>(rhs)) <=
max_ulps;
}
class Fp16EqMatcher {
public:
using is_gtest_matcher = void;
explicit Fp16EqMatcher(float expected) : expected(expected) {}
// Compare the actual value and the expected value stored during
// construction. Omit the explanation opportunity and rely on
// Describe{Negation}To.
bool MatchAndExplain(float actual, std::ostream*) const {
// FP16 only has 10 bits precision while FP32 has 23 bits precision. Thus,
// to check if results of FP16 are almost equal, we could check the result
// is within 4 * 2^13 ULPs of FP32, which equals to 4 ULPs of FP16.
constexpr uint32_t fp16_ulps_in_fp32 = 4 * (1 << 13);
// The minimum exponent of FP16 is 2^-14, which means the minimum ULP of
// FP16 is 2^-24. Therefore, when expected is less than 2^-14, i.e. a
// subnormal FP16 number, the minimum ULP of FP16 should be used instead of
// ULP of FP32.
if (std::abs(expected) < 0x1p-14) {
return std::abs(actual - expected) <= 4 * 0x1p-24;
}
return AlmostEquals(actual, expected, fp16_ulps_in_fp32);
}
// Describes the property of a value matching this matcher.
void DescribeTo(std::ostream* os) const { *os << "is " << expected; }
// Describes the property of a value NOT matching this matcher.
void DescribeNegationTo(std::ostream* os) const {
*os << "is not " << expected;
}
private:
const float expected;
};
} // namespace
testing::Matcher<float> Fp16Eq(float expected) {
return Fp16EqMatcher(expected);
}
std::vector<testing::Matcher<float>> ArrayFp16Eq(std::span<float> values) {
std::vector<testing::Matcher<float>> matchers;
matchers.reserve(values.size());
std::ranges::transform(values, std::back_inserter(matchers), Fp16Eq);
return matchers;
}