diff --git a/BUILD b/BUILD index 9447fe970e..6e13eaa11f 100644 --- a/BUILD +++ b/BUILD @@ -433,6 +433,7 @@ cc_library( copts = COPTS, textual_hdrs = [ "hwy/contrib/math/math-inl.h", + "hwy/contrib/math/f16_math-inl.h", "hwy/contrib/math/fast_math-inl.h", "hwy/contrib/math/fp_arith-inl.h", ], diff --git a/CMakeLists.txt b/CMakeLists.txt index 451dd19c99..11abc8f2b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -240,6 +240,7 @@ list(APPEND HWY_CONTRIB_SOURCES hwy/contrib/hash/cuckoo2x2-inl.h hwy/contrib/image/image.cc hwy/contrib/image/image.h + hwy/contrib/math/f16_math-inl.h hwy/contrib/math/fast_math-inl.h hwy/contrib/intdiv/intdiv-inl.h hwy/contrib/math/fp_arith-inl.h @@ -1029,6 +1030,7 @@ list(APPEND HWY_TEST_FILES hwy/contrib/hash/shardmul_test.cc hwy/contrib/image/image_test.cc hwy/contrib/intdiv/intdiv_test.cc + hwy/contrib/math/f16_math_test.cc hwy/contrib/math/fast_math_test.cc hwy/contrib/math/math_hyper_test.cc hwy/contrib/math/math_tan_test.cc diff --git a/hwy.gni b/hwy.gni index c628e6d062..fa02749042 100644 --- a/hwy.gni +++ b/hwy.gni @@ -52,6 +52,7 @@ hwy_contrib_public = [ "$_hwy/contrib/algo/transform-inl.h", "$_hwy/contrib/dot/dot-inl.h", "$_hwy/contrib/image/image.h", + "$_hwy/contrib/math/f16_math-inl.h", "$_hwy/contrib/math/fast_math-inl.h", "$_hwy/contrib/math/fp_arith-inl.h", "$_hwy/contrib/intdiv/intdiv-inl.h", diff --git a/hwy/contrib/math/f16_math-inl.h b/hwy/contrib/math/f16_math-inl.h new file mode 100644 index 0000000000..4104c94089 --- /dev/null +++ b/hwy/contrib/math/f16_math-inl.h @@ -0,0 +1,250 @@ +// Copyright 2026 Google LLC +// 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. + +// Include guard (still compiled once per target) +#if defined(HIGHWAY_HWY_CONTRIB_MATH_F16_MATH_INL_H_) == \ + defined(HWY_TARGET_TOGGLE) // NOLINT +#ifdef HIGHWAY_HWY_CONTRIB_MATH_F16_MATH_INL_H_ +#undef HIGHWAY_HWY_CONTRIB_MATH_F16_MATH_INL_H_ +#else +#define HIGHWAY_HWY_CONTRIB_MATH_F16_MATH_INL_H_ +#endif + +#include "hwy/contrib/math/math-inl.h" +#include "hwy/highway.h" + +HWY_BEFORE_NAMESPACE(); +namespace hwy { +namespace HWY_NAMESPACE { + +// float16_t overloads of the math functions in math-inl.h. There are no +// float16 kernels yet, so these promote to float32, evaluate the float32 +// kernel, and demote the result. The ops used here support float16_t lanes +// on all targets, even when HWY_HAVE_FLOAT16 is 0. + +// Not named `impl`: unqualified calls such as Log() from inside that +// namespace would also find math-inl.h's impl::Log and be ambiguous. +namespace f16_impl { + +// Promotes lower/upper halves of the float16 vector `v` to float32, +// evaluates `kernel` (a functor wrapping one float32 math function) twice, +// then demotes and recombines. Never instantiated on HWY_SCALAR, so +// PromoteUpperTo and Combine are safe to name here. There is no +// OrderedDemote2To for f32->f16, hence DemoteTo + Combine. +template > +HWY_INLINE V F16ViaF32PerHalf(D d, V v, Kernel kernel) { + const Half dh; + const RepartitionToWide df32; + HWY_DASSERT(Lanes(df32) == Lanes(d) / 2); + const VFromD lo = kernel(df32, PromoteLowerTo(df32, v)); + const VFromD hi = kernel(df32, PromoteUpperTo(df32, v)); + return Combine(d, DemoteTo(dh, hi), DemoteTo(dh, lo)); +} + +// Whether `Rebind` is guaranteed to have exactly `Lanes(d)` lanes, +// i.e. one promotion covers every lane of `v`. +// +// Rebind raises kPow2 by one because float lanes are twice as large. On +// fixed-size targets Lanes() == MaxLanes(), so comparing sizes is enough. On +// scalable targets it is not: MaxLanes() is only an upper bound, so a +// CappedTag whose cap does not bind is a full vector at run time. Rebind then +// runs into the cap, and SVE additionally clamps kPow2 to 0, leaving the +// float32 tag with half as many lanes as `d`. For example, on SVE2 with +// 512-bit vectors, CappedTag has Lanes() == 32, but +// Rebind has Lanes() == 16: the kernel would see only +// lanes 0..15, and DemoteTo, which duplicates its result into both halves of +// the register, would store those same results again for lanes 16..31. +// +// Fractional tags absorb the widening in kPow2 and are safe, as is any tag +// whose cap binds even for the smallest possible vector. Everything else uses +// the per-half path, whose RepartitionToWide keeps kPow2 and therefore always +// has exactly Lanes(d) / 2 lanes. +template +constexpr bool F16OnePromotionCoversAllLanes() { + return (HWY_POW2_D(D) + 1 <= HWY_MAX_POW2) && + (HWY_HAVE_SCALABLE + ? (HWY_POW2_D(D) < 0 || HWY_V_SIZE_D(D) * 2 <= HWY_MIN_BYTES) + : (HWY_V_SIZE_D(D) <= HWY_MAX_BYTES / 2)); +} + +// Evaluates `kernel` on all lanes of `v` with a single promotion and one +// kernel evaluation. +template , HWY_IF_F16_D(D), + hwy::EnableIf()>* = nullptr> +HWY_INLINE V F16ViaF32(D d, V v, Kernel kernel) { + const Rebind df32; + HWY_DASSERT(Lanes(df32) == Lanes(d)); + return DemoteTo(d, kernel(df32, PromoteTo(df32, v))); +} + +// Everything else needs two promotions, one per half. +template , HWY_IF_F16_D(D), + hwy::EnableIf()>* = nullptr> +HWY_INLINE V F16ViaF32(D d, V v, Kernel kernel) { + return F16ViaF32PerHalf(d, v, kernel); +} + +struct ExpKernel { + template + HWY_INLINE VF operator()(DF df, VF x) const { + return Exp(df, x); + } +}; + +struct Exp2Kernel { + template + HWY_INLINE VF operator()(DF df, VF x) const { + return Exp2(df, x); + } +}; + +struct Expm1Kernel { + template + HWY_INLINE VF operator()(DF df, VF x) const { + return Expm1(df, x); + } +}; + +struct LogKernel { + template + HWY_INLINE VF operator()(DF df, VF x) const { + return Log(df, x); + } +}; + +struct Log10Kernel { + template + HWY_INLINE VF operator()(DF df, VF x) const { + return Log10(df, x); + } +}; + +struct Log1pKernel { + template + HWY_INLINE VF operator()(DF df, VF x) const { + return Log1p(df, x); + } +}; + +struct Log2Kernel { + template + HWY_INLINE VF operator()(DF df, VF x) const { + return Log2(df, x); + } +}; + +} // namespace f16_impl + +// The generic templates in math-inl.h are constrained with +// HWY_IF_NOT_SPECIAL_FLOAT_D, so these HWY_IF_F16_D overloads partition the +// overload set rather than being ambiguous. + +/** + * Highway SIMD version of std::exp(x) for float16 lanes. + * + * Valid Lane Types: float16 + * Max Error: ULP = 1 + * Valid Range: float16[-65504, +104] + * @return e^x + */ +template +HWY_INLINE V Exp(D d, V x) { + return f16_impl::F16ViaF32(d, x, f16_impl::ExpKernel()); +} + +/** + * Highway SIMD version of std::exp2(x) for float16 lanes. + * + * Valid Lane Types: float16 + * Max Error: ULP = 1 + * Valid Range: float16[-65504, +128] + * @return 2^x + */ +template +HWY_INLINE V Exp2(D d, V x) { + return f16_impl::F16ViaF32(d, x, f16_impl::Exp2Kernel()); +} + +/** + * Highway SIMD version of std::expm1(x) for float16 lanes. + * + * Valid Lane Types: float16 + * Max Error: ULP = 1 + * Valid Range: float16[-65504, +104] + * @return e^x - 1 + */ +template +HWY_INLINE V Expm1(D d, V x) { + return f16_impl::F16ViaF32(d, x, f16_impl::Expm1Kernel()); +} + +/** + * Highway SIMD version of std::log(x) for float16 lanes. + * + * Valid Lane Types: float16 + * Max Error: ULP = 1 + * Valid Range: float16(0, +65504] + * @return natural logarithm of 'x' + */ +template +HWY_INLINE V Log(D d, V x) { + return f16_impl::F16ViaF32(d, x, f16_impl::LogKernel()); +} + +/** + * Highway SIMD version of std::log10(x) for float16 lanes. + * + * Valid Lane Types: float16 + * Max Error: ULP = 1 + * Valid Range: float16(0, +65504] + * @return base 10 logarithm of 'x' + */ +template +HWY_INLINE V Log10(D d, V x) { + return f16_impl::F16ViaF32(d, x, f16_impl::Log10Kernel()); +} + +/** + * Highway SIMD version of std::log1p(x) for float16 lanes. + * + * Valid Lane Types: float16 + * Max Error: ULP = 1 + * Valid Range: float16[0, +65504] + * @return log(1 + x) + */ +template +HWY_INLINE V Log1p(D d, V x) { + return f16_impl::F16ViaF32(d, x, f16_impl::Log1pKernel()); +} + +/** + * Highway SIMD version of std::log2(x) for float16 lanes. + * + * Valid Lane Types: float16 + * Max Error: ULP = 1 + * Valid Range: float16(0, +65504] + * @return base 2 logarithm of 'x' + */ +template +HWY_INLINE V Log2(D d, V x) { + return f16_impl::F16ViaF32(d, x, f16_impl::Log2Kernel()); +} + +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace hwy +HWY_AFTER_NAMESPACE(); + +#endif // HIGHWAY_HWY_CONTRIB_MATH_F16_MATH_INL_H_ diff --git a/hwy/contrib/math/f16_math_test.cc b/hwy/contrib/math/f16_math_test.cc new file mode 100644 index 0000000000..bf5b5878b6 --- /dev/null +++ b/hwy/contrib/math/f16_math_test.cc @@ -0,0 +1,85 @@ +// Copyright 2026 Google LLC +// 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. + +#include +#include + +#include // std::exp + +// For faster tests. Not using AES, hence NEON_WITHOUT_AES is sufficient. +// SVE is mostly superseded by SVE2. +#ifndef HWY_DISABLED_TARGETS +#define HWY_DISABLED_TARGETS (HWY_NEON | HWY_SVE) +#endif // HWY_DISABLED_TARGETS + +#include "hwy/base.h" + +// clang-format off +#undef HWY_TARGET_INCLUDE +#define HWY_TARGET_INCLUDE "hwy/contrib/math/f16_math_test.cc" +#include "hwy/foreach_target.h" // IWYU pragma: keep +#include "hwy/highway.h" +#include "hwy/contrib/math/f16_math-inl.h" +#include "hwy/contrib/math/math-inl.h" // CallExp +#include "hwy/contrib/math/math_test-inl.h" +// clang-format on + +HWY_BEFORE_NAMESPACE(); +namespace hwy { +namespace HWY_NAMESPACE { +namespace { + +// The float16 min/max bounds mirror the float32 bounds in math_test.cc where +// the kernel is validated (e.g. +104 for Exp), clamped to the float16 finite +// range [-65504, +65504]. The smallest positive float16 subnormal is 2^-24. +// clang-format off +DEFINE_F16_MATH_TEST(Exp, + std::exp, CallExp, -65504.0f, +104.0f, 1) +DEFINE_F16_MATH_TEST(Exp2, + std::exp2, CallExp2, -65504.0f, +128.0f, 1) +DEFINE_F16_MATH_TEST(Expm1, + std::expm1, CallExpm1, -65504.0f, +104.0f, 1) +DEFINE_F16_MATH_TEST(Log, + std::log, CallLog, +5.960464478E-8f, +65504.0f, 1) +DEFINE_F16_MATH_TEST(Log10, + std::log10, CallLog10, +5.960464478E-8f, +65504.0f, 1) +DEFINE_F16_MATH_TEST(Log1p, + std::log1p, CallLog1p, +0.0f, +65504.0f, 1) +DEFINE_F16_MATH_TEST(Log2, + std::log2, CallLog2, +5.960464478E-8f, +65504.0f, 1) +// clang-format on + +} // namespace +// NOLINTNEXTLINE(google-readability-namespace-comments) +} // namespace HWY_NAMESPACE +} // namespace hwy +HWY_AFTER_NAMESPACE(); + +#if HWY_ONCE +namespace hwy { +namespace { +HWY_BEFORE_TEST(HwyF16MathTest); +HWY_EXPORT_AND_TEST_P(HwyF16MathTest, TestAllF16Exp); +HWY_EXPORT_AND_TEST_P(HwyF16MathTest, TestAllF16Exp2); +HWY_EXPORT_AND_TEST_P(HwyF16MathTest, TestAllF16Expm1); +HWY_EXPORT_AND_TEST_P(HwyF16MathTest, TestAllF16Log); +HWY_EXPORT_AND_TEST_P(HwyF16MathTest, TestAllF16Log10); +HWY_EXPORT_AND_TEST_P(HwyF16MathTest, TestAllF16Log1p); +HWY_EXPORT_AND_TEST_P(HwyF16MathTest, TestAllF16Log2); +HWY_AFTER_TEST(); +} // namespace +} // namespace hwy +HWY_TEST_MAIN(); +#endif // HWY_ONCE diff --git a/hwy/contrib/math/math-inl.h b/hwy/contrib/math/math-inl.h index b3d9b86de3..ff32ac1ac3 100644 --- a/hwy/contrib/math/math-inl.h +++ b/hwy/contrib/math/math-inl.h @@ -212,7 +212,7 @@ HWY_NOINLINE V CallErf(const D d, VecArg x) { * Valid Range: float32[-FLT_MAX, +104], float64[-DBL_MAX, +706] * @return e^x */ -template +template HWY_INLINE V Exp(D d, V x); template HWY_NOINLINE V CallExp(const D d, VecArg x) { @@ -227,7 +227,7 @@ HWY_NOINLINE V CallExp(const D d, VecArg x) { * Valid Range: float32[-FLT_MAX, +128], float64[-DBL_MAX, +1024] * @return 2^x */ -template +template HWY_INLINE V Exp2(D d, V x); template HWY_NOINLINE V CallExp2(const D d, VecArg x) { @@ -242,7 +242,7 @@ HWY_NOINLINE V CallExp2(const D d, VecArg x) { * Valid Range: float32[-FLT_MAX, +104], float64[-DBL_MAX, +706] * @return e^x - 1 */ -template +template HWY_INLINE V Expm1(D d, V x); template HWY_NOINLINE V CallExpm1(const D d, VecArg x) { @@ -257,7 +257,7 @@ HWY_NOINLINE V CallExpm1(const D d, VecArg x) { * Valid Range: float32(0, +FLT_MAX], float64(0, +DBL_MAX] * @return natural logarithm of 'x' */ -template +template HWY_INLINE V Log(D d, V x); template HWY_NOINLINE V CallLog(const D d, VecArg x) { @@ -272,7 +272,7 @@ HWY_NOINLINE V CallLog(const D d, VecArg x) { * Valid Range: float32(0, +FLT_MAX], float64(0, +DBL_MAX] * @return base 10 logarithm of 'x' */ -template +template HWY_INLINE V Log10(D d, V x); template HWY_NOINLINE V CallLog10(const D d, VecArg x) { @@ -287,7 +287,7 @@ HWY_NOINLINE V CallLog10(const D d, VecArg x) { * Valid Range: float32[0, +FLT_MAX], float64[0, +DBL_MAX] * @return log(1 + x) */ -template +template HWY_INLINE V Log1p(D d, V x); template HWY_NOINLINE V CallLog1p(const D d, VecArg x) { @@ -302,7 +302,7 @@ HWY_NOINLINE V CallLog1p(const D d, VecArg x) { * Valid Range: float32(0, +FLT_MAX], float64(0, +DBL_MAX] * @return base 2 logarithm of 'x' */ -template +template HWY_INLINE V Log2(D d, V x); template HWY_NOINLINE V CallLog2(const D d, VecArg x) { @@ -2597,7 +2597,9 @@ HWY_INLINE V Erf(const D d, V x) { return Or(result, sign); } -template +template >()>*> HWY_INLINE V Exp(const D d, V x) { using T = TFromD; @@ -2620,7 +2622,9 @@ HWY_INLINE V Exp(const D d, V x) { return IfThenElseZero(Ge(x, kLowerBound), y); } -template +template >()>*> HWY_INLINE V Exp2(const D d, V x) { using T = TFromD; @@ -2639,7 +2643,9 @@ HWY_INLINE V Exp2(const D d, V x) { return IfThenElseZero(Ge(x, kLowerBound), y); } -template +template >()>*> HWY_INLINE V Expm1(const D d, V x) { using T = TFromD; @@ -2665,18 +2671,24 @@ HWY_INLINE V Expm1(const D d, V x) { return IfThenElse(Lt(x, kLowerBound), kNegOne, z); } -template +template >()>*> HWY_INLINE V Log(const D d, V x) { return impl::Log(d, x); } -template +template >()>*> HWY_INLINE V Log10(const D d, V x) { using T = TFromD; return Mul(Log(d, x), Set(d, static_cast(0.4342944819032518276511))); } -template +template >()>*> HWY_INLINE V Log1p(const D d, V x) { using T = TFromD; const V kOne = Set(d, static_cast(+1.0)); @@ -2695,7 +2707,9 @@ HWY_INLINE V Log1p(const D d, V x) { return IfThenElse(not_pole, non_pole, x); } -template +template >()>*> HWY_INLINE V Log2(const D d, V x) { using T = TFromD; return Mul(Log(d, x), Set(d, static_cast(1.44269504088896340735992))); diff --git a/hwy/contrib/math/math_test-inl.h b/hwy/contrib/math/math_test-inl.h index f6156686d0..815a5b49fa 100644 --- a/hwy/contrib/math/math_test-inl.h +++ b/hwy/contrib/math/math_test-inl.h @@ -286,6 +286,131 @@ HWY_NOINLINE void TestMathRelative(const char* name, T (*fx1)(T), }; \ DEFINE_MATH_TEST_FUNC(NAME) +// ULP distance between two float16 values, in the float16 bit domain. +// hwy::detail::ComputeUlpDelta cannot be used here because std::isnan does +// not support float16_t. +HWY_INLINE uint64_t F16UlpDelta(float16_t actual, float16_t expected) { + const uint16_t a_bits = BitCastScalar(actual); + const uint16_t e_bits = BitCastScalar(expected); + if (a_bits == e_bits) return 0; + const bool a_nan = ScalarIsNaN(F32FromF16(actual)); + const bool e_nan = ScalarIsNaN(F32FromF16(expected)); + if (a_nan && e_nan) return 0; + if (a_nan != e_nan) return ~uint64_t{0}; + // Infinities must match exactly (equal bits, handled above), so that e.g. a + // saturating demote cannot pass off 65504 as within 1 ULP of +inf. + if ((a_bits & 0x7FFF) == 0x7C00 || (e_bits & 0x7FFF) == 0x7C00) { + return ~uint64_t{0}; + } + // Map sign-magnitude to a monotonically increasing integer, so that + // adjacent float16 values differ by 1 and -0 maps to the same value as +0. + const int32_t a_ord = (a_bits & 0x8000) + ? (0x8000 - static_cast(a_bits & 0x7FFF)) + : (0x8000 + static_cast(a_bits)); + const int32_t e_ord = (e_bits & 0x8000) + ? (0x8000 - static_cast(e_bits & 0x7FFF)) + : (0x8000 + static_cast(e_bits)); + return static_cast(a_ord >= e_ord ? a_ord - e_ord : e_ord - a_ord); +} + +// Rounds the double-precision reference to float16 with a single rounding +// step, via a round-to-odd float intermediate; a plain double->float->f16 +// conversion double-rounds, which is off by 1 float16 ULP when the double +// falls within half a float ULP of a float16 rounding boundary. Handling +// overflow up front also avoids the (formally undefined) conversion of +// out-of-float-range doubles such as exp(104) to float. +HWY_INLINE float16_t F16FromF64(double value) { + if (ScalarIsNaN(value)) return F16FromF32(static_cast(value)); + // Values of at least 65520 (the float16 overflow threshold) round to inf. + if (value >= 65520.0) return BitCastScalar(uint16_t{0x7C00}); + if (value <= -65520.0) return BitCastScalar(uint16_t{0xFC00}); + const float f = static_cast(value); + if (static_cast(f) == value) return F16FromF32(f); + uint32_t bits = BitCastScalar(f); + // Round to odd: magnitude-truncate if rounding to float moved away from + // zero (also correct across exponent boundaries), then set the LSB. + if (ScalarAbs(static_cast(f)) > ScalarAbs(value)) bits -= 1; + return F16FromF32(BitCastScalar(bits | 1)); +} + +// Exhaustive ULP test for float16 math functions: for every float16 bit +// pattern whose value is within [min, max] (given as float), compares the +// vector function `fxN` against the double-precision scalar reference `fx1` +// rounded to float16. All ops used here support float16_t lanes even when +// HWY_HAVE_FLOAT16 is 0, so this runs on every target. +template +HWY_NOINLINE void TestF16Math(const char* name, double (*fx1)(double), + Vec (*fxN)(D, VecArg>), D d, float min, + float max, uint64_t max_error_ulp) { + if (HWY_MATH_TEST_EXCESS_PRECISION) { + static bool once = true; + if (once) { + once = false; + HWY_WARN("Skipping math_test due to GCC issue with excess precision.\n"); + } + return; + } + + using T = float16_t; + const size_t N = Lanes(d); + auto in_lanes = AllocateAligned(N); + auto actual_lanes = AllocateAligned(N); + HWY_ASSERT(in_lanes && actual_lanes); + uint64_t max_ulp = 0; + // float16 has few enough bit patterns to test them all, in batches of N + // consecutive patterns so that lanes hold distinct values; this also + // verifies lane placement, which a splatted input could not. N divides + // 65536, so base + i never exceeds 0xFFFF. Lanes whose value is NaN or + // outside [min, max] still go through fxN but are skipped when verifying. + for (uint32_t base = 0; base <= 0xFFFF; base += static_cast(N)) { + for (size_t i = 0; i < N; ++i) { + const uint32_t bits = base + static_cast(i); + in_lanes[i] = BitCastScalar(static_cast(bits)); + } + Store(fxN(d, Load(d, in_lanes.get())), d, actual_lanes.get()); + for (size_t i = 0; i < N; ++i) { + const float value_f32 = F32FromF16(in_lanes[i]); + if (ScalarIsNaN(value_f32) || value_f32 < min || value_f32 > max) { + continue; + } + const T expected = F16FromF64(fx1(static_cast(value_f32))); + const uint64_t ulp = F16UlpDelta(actual_lanes[i], expected); + if (ulp > max_error_ulp) { + // Several tags can have the same Lanes() but differ in MaxLanes and + // pow2, so print those too, otherwise the failures are hard to place. + fprintf(stderr, + "%s (MaxLanes %d, pow2 %d): %s(%f) lane %d expected %E " + "actual %E ulp %g max ulp %u\n", + hwy::TypeName(T(), Lanes(d)).c_str(), + static_cast(HWY_MAX_LANES_D(D)), int{HWY_POW2_D(D)}, name, + static_cast(value_f32), static_cast(i), + static_cast(F32FromF16(expected)), + static_cast(F32FromF16(actual_lanes[i])), + static_cast(ulp), static_cast(max_error_ulp)); + } + max_ulp = HWY_MAX(max_ulp, ulp); + } + } + fprintf(stderr, "%s: %s max_ulp %g\n", hwy::TypeName(T(), Lanes(d)).c_str(), + name, static_cast(max_ulp)); + HWY_ASSERT(max_ulp <= max_error_ulp); +} + +// Unlike DEFINE_MATH_TEST, registration does not go through ForFloat16Types, +// which is empty when HWY_HAVE_FLOAT16 is 0: the promote/demote-based f16 +// math functions work on all targets. +#undef DEFINE_F16_MATH_TEST +#define DEFINE_F16_MATH_TEST(NAME, Fx1, FxN, F16_MIN, F16_MAX, F16_ERROR) \ + struct TestF16##NAME { \ + template \ + HWY_NOINLINE void operator()(T, D d) { \ + TestF16Math(HWY_STR(NAME), Fx1, FxN, d, F16_MIN, F16_MAX, F16_ERROR); \ + } \ + }; \ + HWY_NOINLINE void TestAllF16##NAME() { \ + ForPartialVectors()(float16_t()); \ + } + // NOLINTNEXTLINE(google-readability-namespace-comments) } // namespace HWY_NAMESPACE } // namespace hwy diff --git a/hwy_tests.bzl b/hwy_tests.bzl index 554ff98eeb..34a3673c38 100644 --- a/hwy_tests.bzl +++ b/hwy_tests.bzl @@ -67,6 +67,11 @@ HWY_CONTRIB_TESTS = ( "fast_math_test", [":math"], ), + ( + "hwy/contrib/math/", + "f16_math_test", + [":math"], + ), ( "hwy/contrib/math/", "math_hyper_test", diff --git a/meson.build b/meson.build index 8c4a8cef5d..5ec8ff01ed 100644 --- a/meson.build +++ b/meson.build @@ -139,6 +139,7 @@ hwy_contrib_headers = files( 'hwy/contrib/hash/cuckoo2x2.h', 'hwy/contrib/hash/cuckoo2x2-inl.h', 'hwy/contrib/image/image.h', + 'hwy/contrib/math/f16_math-inl.h', 'hwy/contrib/math/fast_math-inl.h', 'hwy/contrib/math/fp_arith-inl.h', 'hwy/contrib/intdiv/intdiv-inl.h', @@ -706,6 +707,7 @@ if tests_enabled 'hwy/contrib/hash/shardmul_test.cc', 'hwy/contrib/image/image_test.cc', 'hwy/contrib/intdiv/intdiv_test.cc', + 'hwy/contrib/math/f16_math_test.cc', 'hwy/contrib/math/fast_math_test.cc', 'hwy/contrib/math/math_hyper_test.cc', 'hwy/contrib/math/math_tan_test.cc',