| // This file is part of Eigen, a lightweight C++ template library | |
| // for linear algebra. | |
| // | |
| // This Source Code Form is subject to the terms of the Mozilla | |
| // Public License v. 2.0. If a copy of the MPL was not distributed | |
| // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. | |
| // SPDX-FileCopyrightText: The Eigen Authors | |
| // SPDX-License-Identifier: MPL-2.0 | |
| // Device-side packet math: test/packetmath.cpp never runs on a GPU. Thread i applies one operation to packet i | |
| // (gpu_common.h's run_on_gpu); the host compares against a reference using bit-exact results, full-bit masks, or a | |
| // named ULP budget. Parts: 1/3 float4/double2 core, 2/4 float4/double2 math, 5/6 half core/math, 7 scalar fallbacks | |
| // and preinterpret. | |
| #define EIGEN_TEST_NO_LONGDOUBLE | |
| #define EIGEN_TEST_NO_COMPLEX | |
| #define EIGEN_USE_GPU | |
| // packetmath_test_shared.h brings in main.h, which has no include guard. | |
| #include "packetmath_test_shared.h" | |
| #include "gpu_common.h" | |
| #include "packetmath_data_movement.h" | |
| #include <algorithm> | |
| #include <cmath> | |
| #include <string> | |
| #include <vector> | |
| // Packet4h2 is an alias of ulonglong2, which exists in both passes but has unpacket_traits only in the device pass: | |
| // eight halves in four half2 lanes. | |
| namespace Eigen { | |
| namespace test { | |
| template <> | |
| struct packet_layout<ulonglong2> { | |
| using Scalar = half; | |
| static constexpr int kSize = 8; | |
| }; | |
| } // namespace test | |
| } // namespace Eigen | |
| namespace packetmath_gpu_test { | |
| using Eigen::Index; | |
| using Eigen::internal::packet_traits; | |
| namespace test = Eigen::test; | |
| using Eigen::numext::bit_cast; | |
| using test::special_values; | |
| using test::Buffer; | |
| using test::packet_layout; | |
| struct MathUlpBudget { | |
| uint64_t log, log1p, exp, exp2, expm1; | |
| }; | |
| // The reference uses the next floating-point type; long double is not wider on MSVC. | |
| template <typename Scalar> | |
| struct wider_type { | |
| using type = double; | |
| }; | |
| template <> | |
| struct wider_type<double> { | |
| using type = long double; | |
| }; | |
| // nvcc compiles sqrtf to a correctly rounded sqrt (-prec-sqrt=true is its default); clang as the CUDA compiler | |
| // lowers it to an approximation one ulp off, and only the __fsqrt_rn intrinsic is exact there. Double sqrt is | |
| // correctly rounded under both. | |
| #if EIGEN_COMP_NVCC | |
| const uint64_t kSqrtFloatUlps = 0; | |
| #else | |
| const uint64_t kSqrtFloatUlps = 1; | |
| #endif | |
| // ------------------------------------------------------------------------------------------------------------------ | |
| // Operations: a static run() per packet type, with device-only bodies (EIGEN_PACKET_TEST_FUNC). | |
| #define EIGEN_GPU_TEST_UNARY_OP(NAME, EXPR) \ | |
| struct NAME { \ | |
| static const char* name() { return #NAME; } \ | |
| template <typename P> \ | |
| EIGEN_PACKET_TEST_FUNC static P run(const P& a) { \ | |
| return EXPR; \ | |
| } \ | |
| }; | |
| #define EIGEN_GPU_TEST_BINARY_OP(NAME, EXPR) \ | |
| struct NAME { \ | |
| static const char* name() { return #NAME; } \ | |
| template <typename P> \ | |
| EIGEN_PACKET_TEST_FUNC static P run(const P& a, const P& b) { \ | |
| return EXPR; \ | |
| } \ | |
| }; | |
| #define EIGEN_GPU_TEST_TERNARY_OP(NAME, EXPR) \ | |
| struct NAME { \ | |
| static const char* name() { return #NAME; } \ | |
| template <typename P> \ | |
| EIGEN_PACKET_TEST_FUNC static P run(const P& a, const P& b, const P& c) { \ | |
| return EXPR; \ | |
| } \ | |
| }; | |
| #define EIGEN_GPU_TEST_REDUX_OP(NAME, EXPR) \ | |
| struct NAME { \ | |
| static const char* name() { return #NAME; } \ | |
| template <typename P> \ | |
| EIGEN_PACKET_TEST_FUNC static typename packet_layout<P>::Scalar run(const P& a) { \ | |
| return EXPR; \ | |
| } \ | |
| }; | |
| EIGEN_GPU_TEST_UNARY_OP(op_identity, a) | |
| EIGEN_GPU_TEST_UNARY_OP(op_pnegate, Eigen::internal::pnegate(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_pconj, Eigen::internal::pconj(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_pabs, Eigen::internal::pabs(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_pfloor, Eigen::internal::pfloor(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_pceil, Eigen::internal::pceil(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_print, Eigen::internal::print(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_ptrunc, Eigen::internal::ptrunc(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_pround, Eigen::internal::pround(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_psqrt, Eigen::internal::psqrt(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_prsqrt, Eigen::internal::prsqrt(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_plog, Eigen::internal::plog(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_plog1p, Eigen::internal::plog1p(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_pexp, Eigen::internal::pexp(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_pexp2, Eigen::internal::pexp2(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_pexpm1, Eigen::internal::pexpm1(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_ptrue, Eigen::internal::ptrue(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_pzero, Eigen::internal::pzero(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_preinterpret_self, Eigen::internal::preinterpret<P>(a)) | |
| EIGEN_GPU_TEST_UNARY_OP(op_psign, Eigen::internal::psign(a)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_padd, Eigen::internal::padd(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_psub, Eigen::internal::psub(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pmul, Eigen::internal::pmul(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pdiv, Eigen::internal::pdiv(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pmin, Eigen::internal::pmin(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pmax, Eigen::internal::pmax(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pmin_numbers, Eigen::internal::pmin<Eigen::PropagateNumbers>(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pmax_numbers, Eigen::internal::pmax<Eigen::PropagateNumbers>(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pmin_nan, Eigen::internal::pmin<Eigen::PropagateNaN>(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pmax_nan, Eigen::internal::pmax<Eigen::PropagateNaN>(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pand, Eigen::internal::pand(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_por, Eigen::internal::por(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pxor, Eigen::internal::pxor(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pandnot, Eigen::internal::pandnot(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pcmp_eq, Eigen::internal::pcmp_eq(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pcmp_lt, Eigen::internal::pcmp_lt(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pcmp_le, Eigen::internal::pcmp_le(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pcmp_lt_or_nan, Eigen::internal::pcmp_lt_or_nan(a, b)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_cmp_ne, a != b ? Eigen::internal::ptrue(a) : Eigen::internal::pzero(a)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_cmp_gt, a > b ? Eigen::internal::ptrue(a) : Eigen::internal::pzero(a)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_cmp_ge, a >= b ? Eigen::internal::ptrue(a) : Eigen::internal::pzero(a)) | |
| EIGEN_GPU_TEST_BINARY_OP(op_pabsdiff, Eigen::internal::pabsdiff(a, b)) | |
| EIGEN_GPU_TEST_TERNARY_OP(op_pmadd, Eigen::internal::pmadd(a, b, c)) | |
| EIGEN_GPU_TEST_TERNARY_OP(op_pselect, Eigen::internal::pselect(a, b, c)) | |
| EIGEN_GPU_TEST_REDUX_OP(op_pfirst, Eigen::internal::pfirst(a)) | |
| EIGEN_GPU_TEST_REDUX_OP(op_predux, Eigen::internal::predux(a)) | |
| EIGEN_GPU_TEST_REDUX_OP(op_predux_mul, Eigen::internal::predux_mul(a)) | |
| EIGEN_GPU_TEST_REDUX_OP(op_predux_min, Eigen::internal::predux_min(a)) | |
| EIGEN_GPU_TEST_REDUX_OP(op_predux_max, Eigen::internal::predux_max(a)) | |
| // ------------------------------------------------------------------------------------------------------------------ | |
| // Kernels: thread i owns packet i. pload<float4> and pload<double2> dereference their argument as the packet, whose | |
| // alignment is Aligned16. Every aligned load and store here addresses a whole number of 16-byte packets past a | |
| // gpuMalloc base, which cudaMalloc documents as "suitably aligned for any kind of variable" (hipMalloc documents no | |
| // alignment). | |
| template <typename Packet, typename Op> | |
| struct unary_kernel { | |
| using Scalar = typename packet_layout<Packet>::Scalar; | |
| static constexpr int kSize = packet_layout<Packet>::kSize; | |
| EIGEN_PACKET_TEST_FUNC void operator()(int i, const Scalar* in, Scalar* out) const { | |
| #if defined(EIGEN_GPU_COMPILE_PHASE) | |
| Eigen::internal::pstore(out + i * kSize, Op::run(Eigen::internal::pload<Packet>(in + i * kSize))); | |
| #endif | |
| } | |
| }; | |
| // The operands of packet i are stored back to back: a at 2i, b at 2i + 1 (and c at 3i + 2 for three operands). | |
| template <typename Packet, typename Op> | |
| struct binary_kernel { | |
| using Scalar = typename packet_layout<Packet>::Scalar; | |
| static constexpr int kSize = packet_layout<Packet>::kSize; | |
| EIGEN_PACKET_TEST_FUNC void operator()(int i, const Scalar* in, Scalar* out) const { | |
| #if defined(EIGEN_GPU_COMPILE_PHASE) | |
| const Packet a = Eigen::internal::pload<Packet>(in + (2 * i) * kSize); | |
| const Packet b = Eigen::internal::pload<Packet>(in + (2 * i + 1) * kSize); | |
| Eigen::internal::pstore(out + i * kSize, Op::run(a, b)); | |
| #endif | |
| } | |
| }; | |
| template <typename Packet, typename Op> | |
| struct ternary_kernel { | |
| using Scalar = typename packet_layout<Packet>::Scalar; | |
| static constexpr int kSize = packet_layout<Packet>::kSize; | |
| EIGEN_PACKET_TEST_FUNC void operator()(int i, const Scalar* in, Scalar* out) const { | |
| #if defined(EIGEN_GPU_COMPILE_PHASE) | |
| const Packet a = Eigen::internal::pload<Packet>(in + (3 * i) * kSize); | |
| const Packet b = Eigen::internal::pload<Packet>(in + (3 * i + 1) * kSize); | |
| const Packet c = Eigen::internal::pload<Packet>(in + (3 * i + 2) * kSize); | |
| Eigen::internal::pstore(out + i * kSize, Op::run(a, b, c)); | |
| #endif | |
| } | |
| }; | |
| template <typename Packet, typename Op> | |
| struct redux_kernel { | |
| using Scalar = typename packet_layout<Packet>::Scalar; | |
| static constexpr int kSize = packet_layout<Packet>::kSize; | |
| EIGEN_PACKET_TEST_FUNC void operator()(int i, const Scalar* in, Scalar* out) const { | |
| #if defined(EIGEN_GPU_COMPILE_PHASE) | |
| out[i] = Op::run(Eigen::internal::pload<Packet>(in + i * kSize)); | |
| #endif | |
| } | |
| }; | |
| template <typename Packet> | |
| struct plset_kernel { | |
| using Scalar = typename packet_layout<Packet>::Scalar; | |
| static constexpr int kSize = packet_layout<Packet>::kSize; | |
| EIGEN_PACKET_TEST_FUNC void operator()(int i, const Scalar* in, Scalar* out) const { | |
| #if defined(EIGEN_GPU_COMPILE_PHASE) | |
| Eigen::internal::pstore(out + i * kSize, Eigen::internal::plset<Packet>(in[i])); | |
| #endif | |
| } | |
| }; | |
| // preinterpret between a scalar and its same-size integer, as the evaluator's cast path uses it on the device. | |
| template <typename Scalar, typename Bits> | |
| struct preinterpret_scalar_kernel { | |
| EIGEN_PACKET_TEST_FUNC void operator()(int i, const Scalar* in, Bits* out) const { | |
| #if defined(EIGEN_GPU_COMPILE_PHASE) | |
| out[i] = Eigen::internal::preinterpret<Bits>(in[i]); | |
| #endif | |
| } | |
| }; | |
| // Casts. The evaluator hands the narrowing form two source packets and the widening form one, from which it | |
| // takes the leading DstPacketSize lanes; these kernels use exactly those two forms. | |
| struct half_to_float_kernel { | |
| EIGEN_PACKET_TEST_FUNC void operator()(int i, const Eigen::half* in, float* out) const { | |
| #if defined(EIGEN_GPU_COMPILE_PHASE) | |
| const ulonglong2 a = Eigen::internal::pload<ulonglong2>(in + 8 * i); | |
| Eigen::internal::pstore(out + 4 * i, Eigen::internal::pcast<ulonglong2, float4>(a)); | |
| #else | |
| EIGEN_UNUSED_VARIABLE(i); | |
| EIGEN_UNUSED_VARIABLE(in); | |
| EIGEN_UNUSED_VARIABLE(out); | |
| #endif | |
| } | |
| }; | |
| struct float_to_half_kernel { | |
| EIGEN_PACKET_TEST_FUNC void operator()(int i, const float* in, Eigen::half* out) const { | |
| #if defined(EIGEN_GPU_COMPILE_PHASE) | |
| const float4 a = Eigen::internal::pload<float4>(in + 8 * i); | |
| const float4 b = Eigen::internal::pload<float4>(in + 8 * i + 4); | |
| Eigen::internal::pstore(out + 8 * i, Eigen::internal::pcast<float4, ulonglong2>(a, b)); | |
| #else | |
| EIGEN_UNUSED_VARIABLE(i); | |
| EIGEN_UNUSED_VARIABLE(in); | |
| EIGEN_UNUSED_VARIABLE(out); | |
| #endif | |
| } | |
| }; | |
| // The device pass's packet traits, written into an int array: the flags the host selects operations by are the | |
| // device's, not the host pass's, where the half packets and their traits do not exist. | |
| #define EIGEN_GPU_TEST_TRAIT_FLAGS(X) \ | |
| X(Vectorizable) \ | |
| X(size) \ | |
| X(HasAdd) \ | |
| X(HasSub) \ | |
| X(HasMul) \ | |
| X(HasDiv) \ | |
| X(HasNegate) \ | |
| X(HasAbs) \ | |
| X(HasMin) \ | |
| X(HasMax) \ | |
| X(HasCmp) \ | |
| X(HasRound) \ | |
| X(HasSqrt) \ | |
| X(HasRsqrt) \ | |
| X(HasSign) X(HasAbsDiff) X(HasSetLinear) X(HasConj) X(HasReciprocal) X(HasExp) X(HasExpm1) X(HasLog) X(HasLog1p) | |
| #define EIGEN_GPU_TEST_TRAIT_NAME(FLAG) #FLAG, | |
| #define EIGEN_GPU_TEST_TRAIT_ENUM(FLAG) k##FLAG, | |
| #define EIGEN_GPU_TEST_TRAIT_VALUE(FLAG) out[k++] = static_cast<int>(packet_traits<Scalar>::FLAG); | |
| const char* const kTraitNames[] = {EIGEN_GPU_TEST_TRAIT_FLAGS(EIGEN_GPU_TEST_TRAIT_NAME)}; | |
| enum Trait { EIGEN_GPU_TEST_TRAIT_FLAGS(EIGEN_GPU_TEST_TRAIT_ENUM) kNumTraits }; | |
| template <typename Scalar> | |
| struct traits_kernel { | |
| EIGEN_PACKET_TEST_FUNC void operator()(int i, const int*, int* out) const { | |
| #if defined(EIGEN_GPU_COMPILE_PHASE) | |
| if (i != 0) return; | |
| int k = 0; | |
| EIGEN_GPU_TEST_TRAIT_FLAGS(EIGEN_GPU_TEST_TRAIT_VALUE) | |
| #endif | |
| } | |
| }; | |
| template <typename Scalar> | |
| std::vector<int> device_traits() { | |
| Buffer<int> dummy(1), report(kNumTraits); | |
| dummy.setZero(); // the kernel ignores it, but it is still copied to the device | |
| report.setConstant(-1); | |
| run_on_gpu(traits_kernel<Scalar>(), 1, dummy, report); | |
| return std::vector<int>(report.data(), report.data() + kNumTraits); | |
| } | |
| // Every flag the device advertises must be covered by a part of this test or deferred to a reserved part by name. | |
| void check_advertised_ops_are_covered(const std::vector<int>& traits, const std::vector<Trait>& covered_here) { | |
| const std::vector<Trait> deferred = {kHasExp, kHasExpm1, kHasLog, kHasLog1p}; | |
| for (int k = kHasAdd; k < kNumTraits; ++k) { | |
| const Trait flag = static_cast<Trait>(k); | |
| if (traits[k] == 0) continue; | |
| const auto covered = [flag](const std::vector<Trait>& list) { | |
| return std::find(list.begin(), list.end(), flag) != list.end(); | |
| }; | |
| const bool ok = covered(covered_here) || covered(deferred); | |
| if (!ok) std::cout << "advertised device op without a test: " << kTraitNames[k] << std::endl; | |
| VERIFY(ok); | |
| } | |
| } | |
| // ------------------------------------------------------------------------------------------------------------------ | |
| // Inputs. | |
| // Every exponent step of the type, at a few mantissas, both signs. | |
| template <typename Scalar> | |
| std::vector<Scalar> exponent_grid(int exponent_step) { | |
| using L = std::numeric_limits<Scalar>; | |
| std::vector<Scalar> values; | |
| const Scalar mantissas[] = {Scalar(1), Scalar(1.25), Scalar(1.5), Scalar(1.75), Scalar(2) - L::epsilon()}; | |
| for (int e = L::min_exponent - L::digits + 1; e <= L::max_exponent - 1; e += exponent_step) { | |
| for (Scalar m : mantissas) { | |
| const Scalar v = std::ldexp(m, e); | |
| if ((std::isfinite)(v) && v != Scalar(0)) { | |
| values.push_back(v); | |
| values.push_back(-v); | |
| } | |
| } | |
| } | |
| return values; | |
| } | |
| template <typename Scalar> | |
| std::vector<Scalar> random_values(int count) { | |
| std::vector<Scalar> values(count); | |
| for (Scalar& v : values) { | |
| v = Eigen::internal::random<Scalar>(Scalar(-1), Scalar(1)) * | |
| std::ldexp(Scalar(1), Eigen::internal::random<int>(-30, 30)); | |
| } | |
| return values; | |
| } | |
| // The special values at every lane position (a run of ones shifts them), then the grid and the random values, | |
| // padded to whole packets with ones. | |
| template <typename Scalar> | |
| Buffer<Scalar> unary_inputs(int packet_size, int random_count) { | |
| std::vector<Scalar> values; | |
| const std::vector<Scalar> specials = special_values<Scalar>(); | |
| for (int shift = 0; shift < packet_size; ++shift) { | |
| values.insert(values.end(), shift, Scalar(1)); | |
| values.insert(values.end(), specials.begin(), specials.end()); | |
| } | |
| const std::vector<Scalar> grid = exponent_grid<Scalar>(std::is_same<Scalar, double>::value ? 8 : 2); | |
| values.insert(values.end(), grid.begin(), grid.end()); | |
| const std::vector<Scalar> random = random_values<Scalar>(random_count); | |
| values.insert(values.end(), random.begin(), random.end()); | |
| while (values.size() % packet_size != 0) values.push_back(Scalar(1)); | |
| return Eigen::Map<const Buffer<Scalar>>(values.data(), values.size()); | |
| } | |
| template <typename Scalar> | |
| struct binary_inputs { | |
| std::vector<Scalar> a, b; | |
| binary_inputs() = default; | |
| // The cross product of the special values, then random pairs, then a shifted copy of the specials so that a | |
| // pair reaches every lane. `exclude` drops pairs the operation leaves implementation-defined. | |
| template <typename Exclude> | |
| binary_inputs(int packet_size, int random_count, Exclude exclude) { | |
| const std::vector<Scalar> specials = special_values<Scalar>(); | |
| for (int shift = 0; shift < packet_size; ++shift) { | |
| for (int k = 0; k < shift; ++k) push(Scalar(1), Scalar(1)); | |
| for (Scalar x : specials) { | |
| for (Scalar y : specials) { | |
| if (!exclude(x, y)) push(x, y); | |
| } | |
| } | |
| } | |
| const std::vector<Scalar> ra = random_values<Scalar>(random_count); | |
| const std::vector<Scalar> rb = random_values<Scalar>(random_count); | |
| for (int k = 0; k < random_count; ++k) { | |
| if (!exclude(ra[k], rb[k])) push(ra[k], rb[k]); | |
| } | |
| while (a.size() % packet_size != 0) push(Scalar(1), Scalar(1)); | |
| } | |
| void push(Scalar x, Scalar y) { | |
| a.push_back(x); | |
| b.push_back(y); | |
| } | |
| int size() const { return int(a.size()); } | |
| // Packet-interleaved device layout: a-packet, b-packet, a-packet, ... | |
| Buffer<Scalar> interleaved(int packet_size) const { | |
| Buffer<Scalar> in(2 * size()); | |
| for (int p = 0; p < size() / packet_size; ++p) { | |
| for (int l = 0; l < packet_size; ++l) { | |
| in[(2 * p) * packet_size + l] = a[p * packet_size + l]; | |
| in[(2 * p + 1) * packet_size + l] = b[p * packet_size + l]; | |
| } | |
| } | |
| return in; | |
| } | |
| }; | |
| // Every value a half can hold, in bit-pattern order. 65536 lanes is 8192 packets, which the device sweeps in | |
| // microseconds, so the unary operations are tested exhaustively rather than sampled. | |
| std::vector<Eigen::half> all_half_values() { | |
| std::vector<Eigen::half> values; | |
| values.reserve(1 << 16); | |
| for (int bits = 0; bits < (1 << 16); ++bits) { | |
| values.push_back(Eigen::numext::bit_cast<Eigen::half>(static_cast<Eigen::numext::uint16_t>(bits))); | |
| } | |
| return values; | |
| } | |
| // Pairs covering the whole domain in both operands: every value against a value a stride away, plus the cross | |
| // product of the special values. 40503 is coprime with 65536, so the second operand also runs over everything. | |
| binary_inputs<Eigen::half> all_half_pairs(bool exclude_nan) { | |
| const std::vector<Eigen::half> values = all_half_values(); | |
| binary_inputs<Eigen::half> pairs; | |
| for (std::size_t i = 0; i < values.size(); ++i) { | |
| const Eigen::half a = values[i], b = values[(i * 40503) % values.size()]; | |
| if (exclude_nan && ((Eigen::numext::isnan)(a) || (Eigen::numext::isnan)(b))) continue; | |
| pairs.push(a, b); | |
| } | |
| const std::vector<float> specials = special_values<float>(); | |
| for (float x : specials) { | |
| for (float y : specials) { | |
| const Eigen::half a(x), b(y); | |
| if (exclude_nan && ((Eigen::numext::isnan)(a) || (Eigen::numext::isnan)(b))) continue; | |
| pairs.push(a, b); | |
| } | |
| } | |
| while (pairs.size() % 8 != 0) pairs.push(Eigen::half(1), Eigen::half(1)); | |
| return pairs; | |
| } | |
| template <typename Scalar> | |
| bool never(Scalar, Scalar) { | |
| return false; | |
| } | |
| template <typename Scalar> | |
| bool either_nan(Scalar x, Scalar y) { | |
| return (std::isnan)(x) || (std::isnan)(y); | |
| } | |
| // ------------------------------------------------------------------------------------------------------------------ | |
| // Comparators. | |
| template <typename Scalar> | |
| struct compare_bits { | |
| bool nan_is_nan; | |
| bool operator()(const Scalar* ref, const Scalar* vec, int n) const { | |
| return test::areEqualBits(ref, vec, n, nan_is_nan); | |
| } | |
| }; | |
| template <typename Scalar> | |
| struct compare_ulps { | |
| uint64_t ulps; | |
| bool operator()(const Scalar* ref, const Scalar* vec, int n) const { return test::areWithinUlps(ref, vec, n, ulps); } | |
| }; | |
| // ------------------------------------------------------------------------------------------------------------------ | |
| // Drivers. | |
| // VERIFY with the operation's name in the failure report. | |
| #define VERIFY_OP(COND) \ | |
| do { \ | |
| const bool ok_ = (COND); \ | |
| if (!ok_) std::cout << "failing operation: " << Op::name() << std::endl; \ | |
| VERIFY(ok_); \ | |
| } while (0) | |
| template <typename Packet, typename Op, typename Ref, typename Compare> | |
| void check_unary(const Buffer<typename packet_layout<Packet>::Scalar>& in, Ref ref, Compare compare) { | |
| using Scalar = typename packet_layout<Packet>::Scalar; | |
| const int kSize = packet_layout<Packet>::kSize; | |
| Buffer<Scalar> out(in.size()); | |
| out.setConstant(Scalar(-7)); | |
| run_on_gpu(unary_kernel<Packet, Op>(), int(in.size()) / kSize, in, out); | |
| Buffer<Scalar> expected(in.size()); | |
| for (Index k = 0; k < in.size(); ++k) expected[k] = ref(in[k]); | |
| VERIFY_OP(compare(expected.data(), out.data(), int(in.size()))); | |
| } | |
| template <typename Packet, typename Op, typename Ref, typename Compare> | |
| void check_binary(const binary_inputs<typename packet_layout<Packet>::Scalar>& inputs, Ref ref, Compare compare) { | |
| using Scalar = typename packet_layout<Packet>::Scalar; | |
| const int kSize = packet_layout<Packet>::kSize; | |
| const Buffer<Scalar> in = inputs.interleaved(kSize); | |
| Buffer<Scalar> out(inputs.size()); | |
| out.setConstant(Scalar(-7)); | |
| run_on_gpu(binary_kernel<Packet, Op>(), inputs.size() / kSize, in, out); | |
| Buffer<Scalar> expected(inputs.size()); | |
| for (int k = 0; k < inputs.size(); ++k) expected[k] = ref(inputs.a[k], inputs.b[k]); | |
| VERIFY_OP(compare(expected.data(), out.data(), inputs.size())); | |
| } | |
| // A comparison must return an all-ones lane where the predicate holds and an all-zero lane elsewhere. | |
| template <typename Packet, typename Op, typename Pred> | |
| void check_compare(const binary_inputs<typename packet_layout<Packet>::Scalar>& inputs, Pred pred) { | |
| using Scalar = typename packet_layout<Packet>::Scalar; | |
| const int kSize = packet_layout<Packet>::kSize; | |
| const Buffer<Scalar> in = inputs.interleaved(kSize); | |
| Buffer<Scalar> out(inputs.size()); | |
| out.setConstant(Scalar(-7)); | |
| run_on_gpu(binary_kernel<Packet, Op>(), inputs.size() / kSize, in, out); | |
| Buffer<bool> zero_mask(inputs.size()); | |
| for (int k = 0; k < inputs.size(); ++k) zero_mask[k] = !pred(inputs.a[k], inputs.b[k]); | |
| VERIFY_OP(test::areFullBitMasks(out.data(), zero_mask.data(), inputs.size())); | |
| } | |
| template <typename Packet, typename Op, typename Compare> | |
| void check_ternary(const std::vector<typename packet_layout<Packet>::Scalar>& a, | |
| const std::vector<typename packet_layout<Packet>::Scalar>& b, | |
| const std::vector<typename packet_layout<Packet>::Scalar>& c, | |
| const Buffer<typename packet_layout<Packet>::Scalar>& expected, Compare compare) { | |
| using Scalar = typename packet_layout<Packet>::Scalar; | |
| const int kSize = packet_layout<Packet>::kSize; | |
| const int n = int(a.size()); | |
| Buffer<Scalar> in(3 * n); | |
| for (int p = 0; p < n / kSize; ++p) { | |
| for (int l = 0; l < kSize; ++l) { | |
| in[(3 * p) * kSize + l] = a[p * kSize + l]; | |
| in[(3 * p + 1) * kSize + l] = b[p * kSize + l]; | |
| in[(3 * p + 2) * kSize + l] = c[p * kSize + l]; | |
| } | |
| } | |
| Buffer<Scalar> out(n); | |
| out.setConstant(Scalar(-7)); | |
| run_on_gpu(ternary_kernel<Packet, Op>(), n / kSize, in, out); | |
| VERIFY_OP(compare(expected.data(), out.data(), n)); | |
| } | |
| template <typename Packet, typename Op, typename Ref, typename Compare> | |
| void check_redux(const Buffer<typename packet_layout<Packet>::Scalar>& in, Ref ref, Compare compare) { | |
| using Scalar = typename packet_layout<Packet>::Scalar; | |
| const int kSize = packet_layout<Packet>::kSize; | |
| const int n = int(in.size()) / kSize; | |
| Buffer<Scalar> out(n); | |
| out.setConstant(Scalar(-7)); | |
| run_on_gpu(redux_kernel<Packet, Op>(), n, in, out); | |
| Buffer<Scalar> expected(n); | |
| for (int p = 0; p < n; ++p) expected[p] = ref(in.data() + p * kSize); | |
| VERIFY_OP(compare(expected.data(), out.data(), n)); | |
| } | |
| // Runs a kernel once per packet on the device, for the shared data-movement cases. | |
| struct launch_on_gpu { | |
| template <typename Kernel, typename Input, typename Output> | |
| void operator()(const Kernel& kernel, int count, const Input& input, Output& output) const { | |
| run_on_gpu(kernel, count, input, output); | |
| } | |
| }; | |
| // plset with every input as a base, large values and negative zero included. `ref` adds a lane index in the type's | |
| // own arithmetic, since a half sum has to round exactly once; that addition need not keep a NaN payload. | |
| template <typename Packet, typename Ref> | |
| void check_plset(const Buffer<typename packet_layout<Packet>::Scalar>& in, Ref ref) { | |
| using Scalar = typename packet_layout<Packet>::Scalar; | |
| const int kSize = packet_layout<Packet>::kSize; | |
| Buffer<Scalar> out(in.size() * kSize), expected(in.size() * kSize); | |
| out.setConstant(Scalar(-7)); | |
| run_on_gpu(plset_kernel<Packet>(), int(in.size()), in, out); | |
| for (Index k = 0; k < out.size(); ++k) { | |
| expected[k] = (k % kSize == 0) ? in[k / kSize] : ref(in[k / kSize], int(k % kSize)); | |
| } | |
| VERIFY(test::areEqualBits(expected.data(), out.data(), int(out.size())) && "plset"); | |
| } | |
| // ptrue and pzero: every bit of every lane set, or every bit cleared. | |
| template <typename Packet, typename Op> | |
| void check_full_mask(const Buffer<typename packet_layout<Packet>::Scalar>& in, bool expect_zero) { | |
| using Scalar = typename packet_layout<Packet>::Scalar; | |
| Buffer<Scalar> out(in.size()); | |
| out.setConstant(Scalar(-7)); | |
| run_on_gpu(unary_kernel<Packet, Op>(), int(in.size()) / packet_layout<Packet>::kSize, in, out); | |
| const Buffer<bool> zero_mask = Buffer<bool>::Constant(in.size(), expect_zero); | |
| VERIFY_OP(test::areFullBitMasks(out.data(), zero_mask.data(), int(in.size()))); | |
| } | |
| // Points where the standard fixes the result exactly, whatever the implementation's accuracy elsewhere. The | |
| // comparison is bitwise, so the sign of a zero counts; NaN matches NaN whatever the payload. | |
| template <typename Packet, typename Op> | |
| void check_special_values( | |
| const std::vector<std::pair<typename packet_layout<Packet>::Scalar, typename packet_layout<Packet>::Scalar>>& | |
| cases) { | |
| using Scalar = typename packet_layout<Packet>::Scalar; | |
| const int kSize = packet_layout<Packet>::kSize; | |
| std::vector<Scalar> in_values, expected_values; | |
| for (const auto& one : cases) { | |
| in_values.push_back(one.first); | |
| expected_values.push_back(one.second); | |
| } | |
| // Pad to a whole number of packets by repeating the first case; only the real cases are compared. | |
| while (in_values.size() % kSize != 0) { | |
| in_values.push_back(cases[0].first); | |
| expected_values.push_back(cases[0].second); | |
| } | |
| const Buffer<Scalar> in = Eigen::Map<const Buffer<Scalar>>(in_values.data(), in_values.size()); | |
| Buffer<Scalar> out(in.size()); | |
| out.setConstant(Scalar(-7)); | |
| run_on_gpu(unary_kernel<Packet, Op>(), int(in.size()) / kSize, in, out); | |
| VERIFY_OP(test::areEqualBits(expected_values.data(), out.data(), int(cases.size()))); | |
| } | |
| // ------------------------------------------------------------------------------------------------------------------ | |
| // Parts 2 and 4: the transcendental operations. Accuracy over the ordinary range is a named ULP budget against a | |
| // host reference; the values the standard fixes are checked exactly. | |
| template <typename Scalar> | |
| Scalar exp_reference(Scalar x) { | |
| return static_cast<Scalar>(std::exp(static_cast<typename wider_type<Scalar>::type>(x))); | |
| } | |
| template <typename Scalar> | |
| Scalar exp2_reference(Scalar x) { | |
| return static_cast<Scalar>(std::exp2(static_cast<typename wider_type<Scalar>::type>(x))); | |
| } | |
| template <typename Scalar> | |
| Scalar expm1_reference(Scalar x) { | |
| return static_cast<Scalar>(std::expm1(static_cast<typename wider_type<Scalar>::type>(x))); | |
| } | |
| template <typename Scalar> | |
| Scalar log_reference(Scalar x) { | |
| return static_cast<Scalar>(std::log(static_cast<typename wider_type<Scalar>::type>(x))); | |
| } | |
| template <typename Scalar> | |
| Scalar log1p_reference(Scalar x) { | |
| return static_cast<Scalar>(std::log1p(static_cast<typename wider_type<Scalar>::type>(x))); | |
| } | |
| template <typename Scalar> | |
| void packetmath_gpu_real_math() { | |
| using Packet = typename packet_traits<Scalar>::type; | |
| const int kSize = packet_layout<Packet>::kSize; | |
| // The CUDA Math API's documented maximum error for each function, plus one for the rounding of the reference from | |
| // the wider type. The Windows CRT expm1 reference can be 3 ulps off, and its long double has no extra precision. | |
| const MathUlpBudget budget = std::is_same<Scalar, double>::value ? MathUlpBudget{2, 2, 2, 2, EIGEN_COMP_MSVC ? 3 : 2} | |
| : MathUlpBudget{2, 2, 3, 3, 2}; | |
| const std::vector<int> traits = device_traits<Scalar>(); | |
| VERIFY_IS_EQUAL(traits[kHasExp], 1); | |
| VERIFY_IS_EQUAL(traits[kHasLog], 1); | |
| const Scalar zero(0), one(1), inf = std::numeric_limits<Scalar>::infinity(); | |
| const Scalar nan = std::numeric_limits<Scalar>::quiet_NaN(); | |
| const Scalar lowest = std::numeric_limits<Scalar>::lowest(), largest = (std::numeric_limits<Scalar>::max)(); | |
| // Inputs the logarithms accept: positive, plus the special values, which the budgeted comparison tolerates | |
| // because a NaN reference matches a NaN result. | |
| Buffer<Scalar> in = unary_inputs<Scalar>(kSize, 1 << 18); | |
| // Seed 1549110488 on Windows: the CRT reference was 3 ulps from the correctly rounded GPU result. | |
| const double regression_input = -0.56978050887642206; | |
| in.conservativeResize(in.size() + kSize); | |
| in.tail(kSize).setConstant(Scalar(regression_input)); | |
| EIGEN_IF_CONSTEXPR (std::is_same<Scalar, double>::value) { | |
| // Keep a 1-ulp check against MPFR (256 bits, round-to-nearest), independent of the CRT reference. | |
| const Buffer<Scalar> regression = in.tail(kSize); | |
| check_unary<Packet, op_pexpm1>( | |
| regression, [](Scalar) { return Scalar(-0.43435041986303746); }, compare_ulps<Scalar>{1}); | |
| } | |
| Buffer<Scalar> positive(in.size()); | |
| for (Index k = 0; k < in.size(); ++k) positive[k] = std::abs(in[k]); | |
| // log1p's domain is (-1, +inf), and the interesting half of it is the run just above -1: |x| - 0.5 never | |
| // reaches below -0.5, so it would leave the argument reduction there untested. Even lanes therefore hold | |
| // -1 + u with u drawn log-uniformly over [2^-digits, 1), which walks (-1, 0) from a single ulp above -1 up to | |
| // 0; odd lanes keep |x| - 0.5, which covers (-0.5, +inf) and carries the special values. | |
| Buffer<Scalar> above_minus_one(in.size()); | |
| for (Index k = 0; k < in.size(); ++k) { | |
| if (k % 2 == 0) { | |
| const int exponent = Eigen::internal::random<int>(1, Eigen::NumTraits<Scalar>::digits()); | |
| const Scalar significand = Eigen::internal::random<Scalar>(Scalar(1), Scalar(2)); | |
| above_minus_one[k] = Scalar(-1) + std::ldexp(significand, -exponent); | |
| } else { | |
| above_minus_one[k] = std::abs(in[k]) - Scalar(0.5); | |
| } | |
| } | |
| check_unary<Packet, op_pexp>(in, exp_reference<Scalar>, compare_ulps<Scalar>{budget.exp}); | |
| check_unary<Packet, op_pexp2>(in, exp2_reference<Scalar>, compare_ulps<Scalar>{budget.exp2}); | |
| check_unary<Packet, op_pexpm1>(in, expm1_reference<Scalar>, compare_ulps<Scalar>{budget.expm1}); | |
| check_unary<Packet, op_plog>(positive, log_reference<Scalar>, compare_ulps<Scalar>{budget.log}); | |
| check_unary<Packet, op_plog1p>(above_minus_one, log1p_reference<Scalar>, compare_ulps<Scalar>{budget.log1p}); | |
| check_special_values<Packet, op_pexp>( | |
| {{zero, one}, {-zero, one}, {-inf, zero}, {inf, inf}, {nan, nan}, {lowest, zero}, {largest, inf}}); | |
| check_special_values<Packet, op_pexp2>( | |
| {{zero, one}, {-zero, one}, {one, Scalar(2)}, {-inf, zero}, {inf, inf}, {nan, nan}}); | |
| // expm1 preserves the sign of a zero, and saturates to -1 at minus infinity. | |
| check_special_values<Packet, op_pexpm1>( | |
| {{zero, zero}, {-zero, -zero}, {-inf, Scalar(-1)}, {inf, inf}, {nan, nan}, {lowest, Scalar(-1)}}); | |
| check_special_values<Packet, op_plog>( | |
| {{one, zero}, {zero, -inf}, {-zero, -inf}, {-one, nan}, {-inf, nan}, {inf, inf}, {nan, nan}}); | |
| check_special_values<Packet, op_plog1p>( | |
| {{zero, zero}, {-zero, -zero}, {Scalar(-1), -inf}, {Scalar(-2), nan}, {-inf, nan}, {inf, inf}, {nan, nan}}); | |
| } | |
| // ------------------------------------------------------------------------------------------------------------------ | |
| // The core of a floating-point packet type: loads and stores, arithmetic, min/max, rounding, bit operations, | |
| // comparisons, select, reductions, sqrt and rsqrt. | |
| template <typename Scalar> | |
| Scalar rsqrt_reference(Scalar x) { | |
| using Wider = typename wider_type<Scalar>::type; | |
| return static_cast<Scalar>(Wider(1) / std::sqrt(static_cast<Wider>(x))); | |
| } | |
| template <typename Scalar> | |
| void packetmath_gpu_real_core() { | |
| using Packet = typename packet_traits<Scalar>::type; | |
| const int kSize = packet_layout<Packet>::kSize; | |
| using Bits = typename Eigen::numext::get_integer_by_size<sizeof(Scalar)>::unsigned_type; | |
| const compare_bits<Scalar> bits{true}; | |
| const compare_bits<Scalar> bits_and_payload{false}; | |
| const auto values = test::areEqual<Scalar>; | |
| // rsqrt is the one approximate intrinsic among the core operations: the CUDA Math API documents 2 ulp for rsqrtf | |
| // and 1 ulp for rsqrt. The float reference rounds 1/sqrt from double. The double reference uses long double, which | |
| // is double itself under MSVC; rounding y = sqrt(x) and then 1/y is still within 1.5 ulp of 1/sqrt(x), so a device | |
| // result within 1 ulp is at most 2 representable steps from the reference. | |
| const uint64_t rsqrt_ulps = std::is_same<Scalar, double>::value ? 2 : 3; | |
| // The device's view of the type, and what this part covers of it. | |
| const std::vector<int> traits = device_traits<Scalar>(); | |
| std::cout << "device packet_traits<" << typeid(Scalar).name() << ">:"; | |
| for (int k = 0; k < kNumTraits; ++k) std::cout << " " << kTraitNames[k] << "=" << traits[k]; | |
| std::cout << std::endl; | |
| VERIFY_IS_EQUAL(traits[kVectorizable], 1); | |
| VERIFY_IS_EQUAL(traits[ksize], kSize); | |
| VERIFY_IS_EQUAL(traits[kHasCmp], 1); | |
| check_advertised_ops_are_covered( | |
| traits, {kHasAdd, kHasSub, kHasMul, kHasDiv, kHasNegate, kHasAbs, kHasMin, kHasMax, kHasCmp, kHasRound, kHasSqrt, | |
| kHasRsqrt, kHasSign, kHasAbsDiff, kHasSetLinear, kHasConj}); | |
| const Buffer<Scalar> in = unary_inputs<Scalar>(kSize, 1 << 18); | |
| // Loads and stores keep every bit, NaN payloads included. | |
| check_unary<Packet, op_identity>( | |
| in, [](Scalar x) { return x; }, bits_and_payload); | |
| test::packetmath_data_movement<Packet>(in, launch_on_gpu()); | |
| check_plset<Packet>(in, [](Scalar base, int lane) { return Scalar(base + Scalar(lane)); }); | |
| // Sign manipulation and rounding: fixed bit for bit, including the sign of a zero. | |
| check_unary<Packet, op_pnegate>( | |
| in, [](Scalar x) { return -x; }, bits); | |
| check_unary<Packet, op_pconj>( | |
| in, [](Scalar x) { return x; }, bits); | |
| check_unary<Packet, op_pabs>( | |
| in, [](Scalar x) { return std::abs(x); }, bits); | |
| check_unary<Packet, op_pfloor>( | |
| in, [](Scalar x) { return std::floor(x); }, bits); | |
| check_unary<Packet, op_pceil>( | |
| in, [](Scalar x) { return std::ceil(x); }, bits); | |
| check_unary<Packet, op_print>( | |
| in, [](Scalar x) { return std::rint(x); }, bits); | |
| check_unary<Packet, op_ptrunc>( | |
| in, [](Scalar x) { return std::trunc(x); }, bits); | |
| check_unary<Packet, op_pround>( | |
| in, [](Scalar x) { return std::round(x); }, bits); | |
| check_unary<Packet, op_preinterpret_self>( | |
| in, [](Scalar x) { return x; }, bits_and_payload); | |
| check_unary<Packet, op_psign>( | |
| in, [](Scalar x) { return Eigen::numext::sign(x); }, bits); | |
| // No ULP budget admits a lost zero sign or an overflow. | |
| const uint64_t kFar = (std::numeric_limits<uint64_t>::max)(); | |
| VERIFY_IS_EQUAL(test::ulp_distance(Scalar(0), Scalar(-0.0)), kFar); | |
| VERIFY_IS_EQUAL(test::ulp_distance(std::numeric_limits<Scalar>::infinity(), (std::numeric_limits<Scalar>::max)()), | |
| kFar); | |
| VERIFY_IS_EQUAL(test::ulp_distance(Scalar(0), std::numeric_limits<Scalar>::denorm_min()), uint64_t(1)); | |
| if (std::is_same<Scalar, float>::value) { | |
| check_unary<Packet, op_psqrt>( | |
| in, [](Scalar x) { return std::sqrt(x); }, compare_ulps<Scalar>{kSqrtFloatUlps}); | |
| } else { | |
| check_unary<Packet, op_psqrt>( | |
| in, [](Scalar x) { return std::sqrt(x); }, bits); | |
| } | |
| check_unary<Packet, op_prsqrt>(in, rsqrt_reference<Scalar>, compare_ulps<Scalar>{rsqrt_ulps}); | |
| check_full_mask<Packet, op_ptrue>(in, /*expect_zero=*/false); | |
| check_full_mask<Packet, op_pzero>(in, /*expect_zero=*/true); | |
| // Arithmetic: IEEE operations, so the host's own results are the reference, bit for bit. | |
| const binary_inputs<Scalar> pairs(kSize, 1 << 17, never<Scalar>); | |
| check_binary<Packet, op_padd>(pairs, test::REF_ADD<Scalar>, bits); | |
| check_binary<Packet, op_psub>(pairs, test::REF_SUB<Scalar>, bits); | |
| check_binary<Packet, op_pmul>(pairs, test::REF_MUL<Scalar>, bits); | |
| check_binary<Packet, op_pdiv>(pairs, test::REF_DIV<Scalar>, bits); | |
| // The sign of a zero difference is not part of pabsdiff's contract. | |
| check_binary<Packet, op_pabsdiff>( | |
| pairs, [](Scalar x, Scalar y) { return x < y ? y - x : x - y; }, values); | |
| // Plain pmin/pmax leave NaN operands implementation-defined (the device uses fminf/fmaxf); the propagating | |
| // variants fix them. | |
| const binary_inputs<Scalar> number_pairs(kSize, 1 << 16, either_nan<Scalar>); | |
| check_binary<Packet, op_pmin>( | |
| number_pairs, [](Scalar x, Scalar y) { return (std::min)(x, y); }, values); | |
| check_binary<Packet, op_pmax>( | |
| number_pairs, [](Scalar x, Scalar y) { return (std::max)(x, y); }, values); | |
| check_binary<Packet, op_pmin_numbers>( | |
| pairs, [](Scalar x, Scalar y) { return std::fmin(x, y); }, values); | |
| check_binary<Packet, op_pmax_numbers>( | |
| pairs, [](Scalar x, Scalar y) { return std::fmax(x, y); }, values); | |
| check_binary<Packet, op_pmin_nan>( | |
| pairs, | |
| [](Scalar x, Scalar y) { | |
| return ((std::isnan)(x) || (std::isnan)(y)) ? std::numeric_limits<Scalar>::quiet_NaN() : (std::min)(x, y); | |
| }, | |
| values); | |
| check_binary<Packet, op_pmax_nan>( | |
| pairs, | |
| [](Scalar x, Scalar y) { | |
| return ((std::isnan)(x) || (std::isnan)(y)) ? std::numeric_limits<Scalar>::quiet_NaN() : (std::max)(x, y); | |
| }, | |
| values); | |
| // Bit operations act on the representation, payloads included. | |
| check_binary<Packet, op_pand>( | |
| pairs, [](Scalar x, Scalar y) { return bit_cast<Scalar>(Bits(bit_cast<Bits>(x) & bit_cast<Bits>(y))); }, | |
| bits_and_payload); | |
| check_binary<Packet, op_por>( | |
| pairs, [](Scalar x, Scalar y) { return bit_cast<Scalar>(Bits(bit_cast<Bits>(x) | bit_cast<Bits>(y))); }, | |
| bits_and_payload); | |
| check_binary<Packet, op_pxor>( | |
| pairs, [](Scalar x, Scalar y) { return bit_cast<Scalar>(Bits(bit_cast<Bits>(x) ^ bit_cast<Bits>(y))); }, | |
| bits_and_payload); | |
| check_binary<Packet, op_pandnot>( | |
| pairs, [](Scalar x, Scalar y) { return bit_cast<Scalar>(Bits(bit_cast<Bits>(x) & ~bit_cast<Bits>(y))); }, | |
| bits_and_payload); | |
| // Comparisons: full-bit masks, false on any NaN operand. | |
| check_compare<Packet, op_pcmp_eq>(pairs, [](Scalar x, Scalar y) { return x == y; }); | |
| check_compare<Packet, op_pcmp_lt>(pairs, [](Scalar x, Scalar y) { return x < y; }); | |
| check_compare<Packet, op_pcmp_le>(pairs, [](Scalar x, Scalar y) { return x <= y; }); | |
| check_compare<Packet, op_pcmp_lt_or_nan>(pairs, [](Scalar x, Scalar y) { return !(x >= y); }); | |
| { | |
| // pmadd may or may not be contracted by the device compiler (ptxas fuses a*b+c by default): either the | |
| // fused or the separately rounded result is acceptable until the backend provides an explicit fma. | |
| const int count = 1 << 16; | |
| std::vector<Scalar> a = random_values<Scalar>(count), b = random_values<Scalar>(count), | |
| c = random_values<Scalar>(count); | |
| const Scalar epsilon = NumTraits<Scalar>::epsilon(); | |
| const int rounding_case = int(a.size()); | |
| a.push_back(Scalar(1) + epsilon); | |
| b.push_back(Scalar(1) - epsilon); | |
| c.push_back(Scalar(-1)); | |
| const std::vector<Scalar> specials = special_values<Scalar>(); | |
| for (Scalar x : specials) { | |
| for (Scalar y : specials) { | |
| a.push_back(x); | |
| b.push_back(y); | |
| c.push_back(specials[(a.size() * 7) % specials.size()]); | |
| } | |
| } | |
| while (a.size() % kSize != 0) { | |
| a.push_back(Scalar(1)); | |
| b.push_back(Scalar(1)); | |
| c.push_back(Scalar(1)); | |
| } | |
| const int n3 = int(a.size()); | |
| Buffer<Scalar> fused(n3), unfused(n3); | |
| for (int k = 0; k < n3; ++k) { | |
| fused[k] = std::fma(a[k], b[k], c[k]); | |
| // A statement boundary alone still permits contraction with -ffp-contract=fast. | |
| const volatile Scalar product = a[k] * b[k]; | |
| unfused[k] = product + c[k]; | |
| } | |
| VERIFY_IS_EQUAL(fused[rounding_case], -epsilon * epsilon); | |
| VERIFY_IS_EQUAL(unfused[rounding_case], Scalar(0)); | |
| const auto fused_or_not = [&](const Scalar* ref, const Scalar* vec, int m) { | |
| for (int k = 0; k < m; ++k) { | |
| const bool ok = test::ulp_distance(ref[k], vec[k]) == 0 || test::ulp_distance(unfused[k], vec[k]) == 0; | |
| if (!ok) { | |
| std::cout << "pmadd lane " << k << ": " << vec[k] << " is neither fma " << ref[k] << " nor a*b+c " | |
| << unfused[k] << std::endl; | |
| return false; | |
| } | |
| } | |
| return true; | |
| }; | |
| check_ternary<Packet, op_pmadd>(a, b, c, fused, fused_or_not); | |
| // pselect with full-bit masks picks the second operand where the mask is set, bit for bit. | |
| std::vector<Scalar> mask(n3); | |
| Buffer<Scalar> selected(n3); | |
| const Scalar all_ones = Eigen::numext::bit_cast<Scalar>(Bits(~Bits(0))); | |
| for (int k = 0; k < n3; ++k) { | |
| mask[k] = Eigen::internal::random<bool>() ? all_ones : Scalar(0); | |
| selected[k] = Eigen::numext::bit_cast<Bits>(mask[k]) != Bits(0) ? a[k] : b[k]; | |
| } | |
| check_ternary<Packet, op_pselect>(mask, a, b, selected, bits_and_payload); | |
| } | |
| // Reductions. Sums and products follow the device's lane order, which the host reproduces. | |
| check_redux<Packet, op_pfirst>( | |
| in, [](const Scalar* p) { return p[0]; }, bits_and_payload); | |
| check_redux<Packet, op_predux>( | |
| in, | |
| [kSize](const Scalar* p) { | |
| Scalar s = p[0]; | |
| for (int l = 1; l < kSize; ++l) s = s + p[l]; | |
| return s; | |
| }, | |
| bits); | |
| check_redux<Packet, op_predux_mul>( | |
| in, | |
| [kSize](const Scalar* p) { | |
| Scalar s = p[0]; | |
| for (int l = 1; l < kSize; ++l) s = s * p[l]; | |
| return s; | |
| }, | |
| bits); | |
| Buffer<Scalar> numbers(in.size()); | |
| for (Index k = 0; k < in.size(); ++k) numbers[k] = (std::isnan)(in[k]) ? Scalar(1) : in[k]; | |
| check_redux<Packet, op_predux_min>( | |
| numbers, | |
| [kSize](const Scalar* p) { | |
| Scalar s = p[0]; | |
| for (int l = 1; l < kSize; ++l) s = (std::min)(s, p[l]); | |
| return s; | |
| }, | |
| values); | |
| check_redux<Packet, op_predux_max>( | |
| numbers, | |
| [kSize](const Scalar* p) { | |
| Scalar s = p[0]; | |
| for (int l = 1; l < kSize; ++l) s = (std::max)(s, p[l]); | |
| return s; | |
| }, | |
| values); | |
| } | |
| // ------------------------------------------------------------------------------------------------------------------ | |
| // Parts 5 and 6: the half packets. Packet4h2 is eight halves in four half2 lanes and exists only in the device | |
| // pass. References are computed in float and rounded once, which is exact for the arithmetic operations because a | |
| // float mantissa is at least twice a half's plus two bits. | |
| using HalfPacket = ulonglong2; | |
| // A half operation whose reference is the float computation rounded back. | |
| Eigen::half half_of(float x) { return Eigen::half(x); } | |
| Eigen::half half_add(Eigen::half a, Eigen::half b) { return half_of(float(a) + float(b)); } | |
| Eigen::half half_mul(Eigen::half a, Eigen::half b) { return half_of(float(a) * float(b)); } | |
| void packetmath_gpu_half_core() { | |
| using Scalar = Eigen::half; | |
| using Packet = HalfPacket; | |
| using Bits = uint16_t; | |
| const int kSize = packet_layout<Packet>::kSize; | |
| const compare_bits<Scalar> bits{true}; | |
| const compare_bits<Scalar> bits_and_payload{false}; | |
| const auto values = test::areEqual<Scalar>; | |
| const std::vector<int> traits = device_traits<Scalar>(); | |
| std::cout << "device packet_traits<half>:"; | |
| for (int k = 0; k < kNumTraits; ++k) std::cout << " " << kTraitNames[k] << "=" << traits[k]; | |
| std::cout << std::endl; | |
| VERIFY_IS_EQUAL(traits[kVectorizable], 1); | |
| VERIFY_IS_EQUAL(traits[ksize], kSize); | |
| // The hygiene sweep turned these off: there is no rounding operation and no psign for the half packets. | |
| VERIFY_IS_EQUAL(traits[kHasRound], 0); | |
| VERIFY_IS_EQUAL(traits[kHasSign], 0); | |
| check_advertised_ops_are_covered(traits, {kHasAdd, kHasSub, kHasMul, kHasDiv, kHasNegate, kHasAbs, kHasMin, kHasMax, | |
| kHasSqrt, kHasRsqrt, kHasSetLinear, kHasConj, kHasAbsDiff}); | |
| // Exhaustive: every value a half can hold. | |
| const std::vector<Scalar> all = all_half_values(); | |
| const Buffer<Scalar> in = Eigen::Map<const Buffer<Scalar>>(all.data(), all.size()); | |
| check_unary<Packet, op_identity>( | |
| in, [](Scalar x) { return x; }, bits_and_payload); | |
| // The sign of a NaN survives neither __hneg2 nor __habs2, as on the float packets; the value stays NaN. | |
| check_unary<Packet, op_pnegate>( | |
| in, [](Scalar x) { return bit_cast<Scalar>(Bits(bit_cast<Bits>(x) ^ 0x8000u)); }, bits); | |
| check_unary<Packet, op_pabs>( | |
| in, [](Scalar x) { return bit_cast<Scalar>(Bits(bit_cast<Bits>(x) & 0x7fffu)); }, bits); | |
| check_unary<Packet, op_pconj>( | |
| in, [](Scalar x) { return x; }, bits_and_payload); | |
| // The half2 intrinsics quiet a signalling NaN, so data movement matches NaNs by value. The lane index is added in | |
| // half, so the plset reference rounds once, as the packet does. | |
| test::packetmath_data_movement<Packet>(in, launch_on_gpu(), /*nan_payloads=*/false); | |
| check_plset<Packet>(in, [](Scalar base, int lane) { return half_add(base, half_of(float(lane))); }); | |
| // Arithmetic. Rounding once from float is the correctly rounded half result. | |
| const binary_inputs<Scalar> pairs = all_half_pairs(/*exclude_nan=*/false); | |
| const binary_inputs<Scalar> numbers = all_half_pairs(/*exclude_nan=*/true); | |
| check_binary<Packet, op_padd>(pairs, half_add, bits); | |
| check_binary<Packet, op_psub>( | |
| pairs, [](Scalar x, Scalar y) { return half_of(float(x) - float(y)); }, bits); | |
| check_binary<Packet, op_pmul>(pairs, half_mul, bits); | |
| check_binary<Packet, op_pdiv>( | |
| pairs, [](Scalar x, Scalar y) { return half_of(float(x) / float(y)); }, bits); | |
| // Plain min/max leave a NaN operand implementation-defined, as for the float packets. | |
| check_binary<Packet, op_pmin>( | |
| numbers, [](Scalar x, Scalar y) { return float(x) < float(y) ? x : y; }, values); | |
| check_binary<Packet, op_pmax>( | |
| numbers, [](Scalar x, Scalar y) { return float(x) > float(y) ? x : y; }, values); | |
| // Bit operations act on the 16-bit representation. | |
| check_binary<Packet, op_pand>( | |
| pairs, [](Scalar x, Scalar y) { return bit_cast<Scalar>(Bits(bit_cast<Bits>(x) & bit_cast<Bits>(y))); }, | |
| bits_and_payload); | |
| check_binary<Packet, op_por>( | |
| pairs, [](Scalar x, Scalar y) { return bit_cast<Scalar>(Bits(bit_cast<Bits>(x) | bit_cast<Bits>(y))); }, | |
| bits_and_payload); | |
| check_binary<Packet, op_pxor>( | |
| pairs, [](Scalar x, Scalar y) { return bit_cast<Scalar>(Bits(bit_cast<Bits>(x) ^ bit_cast<Bits>(y))); }, | |
| bits_and_payload); | |
| check_binary<Packet, op_pandnot>( | |
| pairs, [](Scalar x, Scalar y) { return bit_cast<Scalar>(Bits(bit_cast<Bits>(x) & ~bit_cast<Bits>(y))); }, | |
| bits_and_payload); | |
| // Comparisons produce all-ones or all-zero 16-bit lanes. | |
| check_compare<Packet, op_pcmp_eq>(pairs, [](Scalar x, Scalar y) { return float(x) == float(y); }); | |
| check_compare<Packet, op_pcmp_lt>(pairs, [](Scalar x, Scalar y) { return float(x) < float(y); }); | |
| check_compare<Packet, op_pcmp_le>(pairs, [](Scalar x, Scalar y) { return float(x) <= float(y); }); | |
| check_full_mask<Packet, op_ptrue>(in, /*expect_zero=*/false); | |
| check_full_mask<Packet, op_pzero>(in, /*expect_zero=*/true); | |
| { | |
| // pselect with full-bit masks picks lane by lane. | |
| const int count = numbers.size(); | |
| std::vector<Scalar> mask(count); | |
| Buffer<Scalar> selected(count); | |
| for (int k = 0; k < count; ++k) { | |
| const bool set = Eigen::internal::random<bool>(); | |
| mask[k] = set ? bit_cast<Scalar>(Bits(0xffffu)) : bit_cast<Scalar>(Bits(0x0000u)); | |
| selected[k] = set ? numbers.a[k] : numbers.b[k]; | |
| } | |
| check_ternary<Packet, op_pselect>(mask, numbers.a, numbers.b, selected, bits_and_payload); | |
| } | |
| { | |
| // pmadd is __hfma2: one rounding, so the reference is the float fma rounded once. | |
| const int count = numbers.size(); | |
| Buffer<Scalar> expected(count); | |
| for (int k = 0; k < count; ++k) { | |
| expected[k] = half_of(std::fma(float(numbers.a[k]), float(numbers.b[k]), float(numbers.a[k]))); | |
| } | |
| check_ternary<Packet, op_pmadd>(numbers.a, numbers.b, numbers.a, expected, bits); | |
| } | |
| // Adjacent opposite-sign lanes cancel before any cross-pair addition can overflow. | |
| Buffer<Scalar> cancellation(8); | |
| cancellation << Scalar(60000), Scalar(-60000), Scalar(60000), Scalar(-60000), Scalar(0), Scalar(0), Scalar(0), | |
| Scalar(0); | |
| check_redux<Packet, op_predux>( | |
| cancellation, [](const Scalar*) { return Scalar(0); }, bits); | |
| // Reductions follow the lane tree of the implementation, so the reference rounds in the same order. | |
| const Buffer<Scalar> finite = [&] { | |
| std::vector<Scalar> v; | |
| for (Index k = 0; k < in.size(); ++k) { | |
| const float x = float(in[k]); | |
| v.push_back((std::isfinite)(x) && std::abs(x) < 4.0f ? in[k] : Scalar(1)); | |
| } | |
| return Eigen::Map<const Buffer<Scalar>>(v.data(), v.size()).eval(); | |
| }(); | |
| check_redux<Packet, op_predux>( | |
| finite, | |
| [](const Scalar* p) { | |
| return half_add(half_add(half_add(half_add(p[0], p[1]), half_add(p[2], p[3])), half_add(p[4], p[5])), | |
| half_add(p[6], p[7])); | |
| }, | |
| bits); | |
| check_redux<Packet, op_predux_mul>( | |
| finite, | |
| [](const Scalar* p) { | |
| return half_mul(half_mul(half_mul(p[0], p[2]), half_mul(p[4], p[6])), | |
| half_mul(half_mul(p[1], p[3]), half_mul(p[5], p[7]))); | |
| }, | |
| bits); | |
| check_redux<Packet, op_predux_min>( | |
| finite, | |
| [](const Scalar* p) { | |
| Scalar m = p[0]; | |
| for (int l = 1; l < 8; ++l) m = float(p[l]) < float(m) ? p[l] : m; | |
| return m; | |
| }, | |
| values); | |
| check_redux<Packet, op_predux_max>( | |
| finite, | |
| [](const Scalar* p) { | |
| Scalar m = p[0]; | |
| for (int l = 1; l < 8; ++l) m = float(p[l]) > float(m) ? p[l] : m; | |
| return m; | |
| }, | |
| values); | |
| check_redux<Packet, op_pfirst>( | |
| in, [](const Scalar* p) { return p[0]; }, bits_and_payload); | |
| // Casts, in the two forms the evaluator uses. | |
| { | |
| const int packets = int(in.size()) / kSize; | |
| Buffer<float> as_float(packets * 4); | |
| as_float.setConstant(-7.0f); | |
| run_on_gpu(half_to_float_kernel(), packets, in, as_float); | |
| for (int k = 0; k < packets * 4; ++k) { | |
| const float want = static_cast<float>(in[(k / 4) * kSize + (k % 4)]); | |
| VERIFY(test::areEqualBits(&want, as_float.data() + k, 1) && "pcast<Packet4h2, float4>"); | |
| } | |
| // The narrowing cast has to round, so its inputs must not be representable as half to begin with: a float | |
| // widened from a half is, and would let any rounding rule pass. Every tie of the half grid is generated | |
| // instead -- the midpoint between each pair of adjacent halves, from the subnormals up to the largest finite | |
| // one -- with the neighbours of each tie, then the overflow and underflow boundaries. | |
| std::vector<float> float_values; | |
| for (unsigned bits = 0; bits + 1 < 0x7c00u; ++bits) { | |
| const float below = float(bit_cast<Scalar>(Bits(bits))); | |
| const float above = float(bit_cast<Scalar>(Bits(bits + 1))); | |
| const float tie = 0.5f * (below + above); // rounds to even, so to one of the two only every other time | |
| for (float value : {tie, std::nextafterf(tie, below), std::nextafterf(tie, above)}) { | |
| float_values.push_back(value); | |
| float_values.push_back(-value); | |
| } | |
| } | |
| const float largest_half = float(bit_cast<Scalar>(Bits(0x7bffu))); | |
| const float overflow_tie = 65520.0f; // halfway from the largest half to where the next one would be | |
| for (float value : | |
| {largest_half, std::nextafterf(largest_half, 1e30f), std::nextafterf(overflow_tie, 0.0f), overflow_tie, | |
| std::nextafterf(overflow_tie, 1e30f), 65536.0f, (std::numeric_limits<float>::max)(), | |
| std::numeric_limits<float>::infinity(), std::numeric_limits<float>::denorm_min(), 0.0f}) { | |
| float_values.push_back(value); | |
| float_values.push_back(-value); | |
| } | |
| while (float_values.size() % 8 != 0) float_values.push_back(0.0f); | |
| const int cast_packets = int(float_values.size()) / 8; | |
| const Buffer<float> floats = Eigen::Map<const Buffer<float>>(float_values.data(), Index(float_values.size())); | |
| Buffer<Scalar> as_half(float_values.size()); | |
| as_half.setConstant(Scalar(-7)); | |
| run_on_gpu(float_to_half_kernel(), cast_packets, floats, as_half); | |
| Buffer<Scalar> want(float_values.size()); | |
| for (Index k = 0; k < want.size(); ++k) want[k] = half_of(floats[k]); | |
| VERIFY(test::areEqualBits(want.data(), as_half.data(), int(want.size())) && "pcast<float4, Packet4h2>"); | |
| } | |
| } | |
| void packetmath_gpu_half_math() { | |
| using Scalar = Eigen::half; | |
| using Packet = HalfPacket; | |
| using Bits = uint16_t; | |
| const std::vector<Scalar> all = all_half_values(); | |
| const Buffer<Scalar> in = Eigen::Map<const Buffer<Scalar>>(all.data(), all.size()); | |
| Buffer<Scalar> positive(in.size()); | |
| for (Index k = 0; k < in.size(); ++k) positive[k] = bit_cast<Scalar>(Bits(bit_cast<Bits>(in[k]) & 0x7fffu)); | |
| // h2sqrt, h2rsqrt, h2log and h2exp against the float computation rounded to half. The device functions are not | |
| // documented to be correctly rounded in half, so allow one half ULP. | |
| const uint64_t kHalfMathUlps = 1; | |
| check_unary<Packet, op_psqrt>( | |
| positive, [](Scalar x) { return half_of(std::sqrt(float(x))); }, compare_ulps<Scalar>{kHalfMathUlps}); | |
| check_unary<Packet, op_prsqrt>( | |
| positive, [](Scalar x) { return half_of(1.0f / std::sqrt(float(x))); }, compare_ulps<Scalar>{kHalfMathUlps}); | |
| check_unary<Packet, op_plog>( | |
| positive, [](Scalar x) { return half_of(std::log(float(x))); }, compare_ulps<Scalar>{kHalfMathUlps}); | |
| check_unary<Packet, op_pexp>( | |
| in, [](Scalar x) { return half_of(std::exp(float(x))); }, compare_ulps<Scalar>{kHalfMathUlps}); | |
| check_unary<Packet, op_plog1p>( | |
| in, [](Scalar x) { return half_of(std::log1p(float(x))); }, compare_ulps<Scalar>{kHalfMathUlps}); | |
| check_unary<Packet, op_pexpm1>( | |
| in, [](Scalar x) { return half_of(std::expm1(float(x))); }, compare_ulps<Scalar>{kHalfMathUlps}); | |
| const Scalar zero(0.0f), one(1.0f), inf = std::numeric_limits<Scalar>::infinity(); | |
| const Scalar nan = std::numeric_limits<Scalar>::quiet_NaN(); | |
| check_special_values<Packet, op_pexp>({{zero, one}, {-zero, one}, {-inf, zero}, {inf, inf}, {nan, nan}}); | |
| check_special_values<Packet, op_plog>({{one, zero}, {zero, -inf}, {-one, nan}, {inf, inf}, {nan, nan}}); | |
| check_special_values<Packet, op_psqrt>({{zero, zero}, {-zero, -zero}, {one, one}, {inf, inf}, {nan, nan}}); | |
| } | |
| // ------------------------------------------------------------------------------------------------------------------ | |
| // Part 7: types without a device packet run the scalar fallbacks of GenericPacketMath.h, which must agree with the | |
| // host's scalar fallbacks bit for bit; preinterpret is the bit_cast the cast path relies on. | |
| template <typename Scalar> | |
| void check_scalar_fallback_common(const binary_inputs<Scalar>& pairs, const Buffer<Scalar>& in) { | |
| const compare_bits<Scalar> bits{true}; | |
| const std::vector<int> traits = device_traits<Scalar>(); | |
| VERIFY_IS_EQUAL(traits[kVectorizable], 0); | |
| VERIFY_IS_EQUAL(traits[ksize], 1); | |
| check_binary<Scalar, op_padd>(pairs, test::REF_ADD<Scalar>, bits); | |
| check_binary<Scalar, op_pmul>(pairs, test::REF_MUL<Scalar>, bits); | |
| check_binary<Scalar, op_pmin>( | |
| pairs, [](Scalar x, Scalar y) { return Eigen::internal::pmin(x, y); }, bits); | |
| check_binary<Scalar, op_pmax>( | |
| pairs, [](Scalar x, Scalar y) { return Eigen::internal::pmax(x, y); }, bits); | |
| check_binary<Scalar, op_pand>( | |
| pairs, [](Scalar x, Scalar y) { return Eigen::internal::pand(x, y); }, bits); | |
| check_binary<Scalar, op_por>( | |
| pairs, [](Scalar x, Scalar y) { return Eigen::internal::por(x, y); }, bits); | |
| check_binary<Scalar, op_pxor>( | |
| pairs, [](Scalar x, Scalar y) { return Eigen::internal::pxor(x, y); }, bits); | |
| check_binary<Scalar, op_pcmp_eq>( | |
| pairs, [](Scalar x, Scalar y) { return Eigen::internal::pcmp_eq(x, y); }, bits); | |
| check_binary<Scalar, op_pcmp_lt>( | |
| pairs, [](Scalar x, Scalar y) { return Eigen::internal::pcmp_lt(x, y); }, bits); | |
| check_binary<Scalar, op_pcmp_le>( | |
| pairs, [](Scalar x, Scalar y) { return Eigen::internal::pcmp_le(x, y); }, bits); | |
| check_unary<Scalar, op_identity>( | |
| in, [](Scalar x) { return x; }, bits); | |
| { | |
| // pselect on a scalar is a ternary on the mask's truth value. | |
| const int n = pairs.size(); | |
| std::vector<Scalar> mask(n); | |
| Buffer<Scalar> selected(n); | |
| for (int k = 0; k < n; ++k) { | |
| mask[k] = Eigen::internal::random<bool>() ? Eigen::internal::ptrue(Scalar(0)) : Eigen::internal::pzero(Scalar(0)); | |
| selected[k] = Eigen::internal::pselect(Scalar(mask[k]), Scalar(pairs.a[k]), Scalar(pairs.b[k])); | |
| } | |
| check_ternary<Scalar, op_pselect>(mask, pairs.a, pairs.b, selected, bits); | |
| } | |
| } | |
| template <typename Scalar> | |
| binary_inputs<Scalar> integer_pairs(int count) { | |
| binary_inputs<Scalar> pairs; | |
| // Signed operands stay within 100 in magnitude, so their sums and products stay in range; unsigned operands reach | |
| // 200, whose uint8_t sums and products wrap, as the REF_* references do. | |
| const Scalar bound = Scalar(NumTraits<Scalar>::IsSigned ? 100 : 200); | |
| for (int k = 0; k < count; ++k) { | |
| pairs.push(Eigen::internal::random<Scalar>(NumTraits<Scalar>::IsSigned ? Scalar(-bound) : Scalar(0), bound), | |
| Eigen::internal::random<Scalar>(NumTraits<Scalar>::IsSigned ? Scalar(-bound) : Scalar(0), bound)); | |
| } | |
| return pairs; | |
| } | |
| template <typename Scalar> | |
| void packetmath_gpu_integer_fallback() { | |
| const compare_bits<Scalar> bits{true}; | |
| const binary_inputs<Scalar> pairs = integer_pairs<Scalar>(1 << 12); | |
| const Buffer<Scalar> in = Eigen::Map<const Buffer<Scalar>>(pairs.a.data(), pairs.size()); | |
| check_scalar_fallback_common<Scalar>(pairs, in); | |
| check_unary<Scalar, op_pnegate>(in, test::negate<Scalar>, bits); | |
| check_unary<Scalar, op_pabs>( | |
| in, [](Scalar x) { return Eigen::internal::pabs(x); }, bits); | |
| check_binary<Scalar, op_psub>(pairs, test::REF_SUB<Scalar>, bits); | |
| binary_inputs<Scalar> nonzero = pairs; | |
| for (Scalar& y : nonzero.b) { | |
| if (y == Scalar(0)) y = Scalar(1); | |
| } | |
| check_binary<Scalar, op_pdiv>(nonzero, test::REF_DIV<Scalar>, bits); | |
| check_binary<Scalar, op_pandnot>( | |
| pairs, [](Scalar x, Scalar y) { return Eigen::internal::pandnot(x, y); }, bits); | |
| } | |
| void packetmath_gpu_bool_fallback() { | |
| binary_inputs<bool> pairs; | |
| for (int k = 0; k < 1 << 10; ++k) pairs.push(Eigen::internal::random<bool>(), Eigen::internal::random<bool>()); | |
| // std::vector<bool> has no data(): copy lane by lane. | |
| Buffer<bool> in(pairs.size()); | |
| for (int k = 0; k < pairs.size(); ++k) in[k] = pairs.a[k]; | |
| check_scalar_fallback_common<bool>(pairs, in); | |
| } | |
| void packetmath_gpu_bfloat16_fallback() { | |
| using Scalar = Eigen::bfloat16; | |
| const compare_bits<Scalar> bits{true}; | |
| std::vector<Scalar> specials; | |
| for (float x : special_values<float>()) specials.push_back(Scalar(x)); | |
| // float denorm_min rounds to 0x0000 and float min - denorm_min rounds to 0x0080, so add raw bfloat16 subnormals. | |
| for (uint16_t raw : | |
| {uint16_t(0x0001), uint16_t(0x0002), uint16_t(0x007f), uint16_t(0x8001), uint16_t(0x8002), uint16_t(0x807f)}) { | |
| specials.push_back(bit_cast<Scalar>(raw)); | |
| } | |
| binary_inputs<Scalar> pairs; | |
| for (Scalar x : specials) { | |
| for (Scalar y : specials) pairs.push(x, y); | |
| } | |
| for (int k = 0; k < 1 << 12; ++k) { | |
| pairs.push(Scalar(Eigen::internal::random<float>(-4.0f, 4.0f)), | |
| Scalar(Eigen::internal::random<float>(-4.0f, 4.0f))); | |
| } | |
| const Buffer<Scalar> in = Eigen::Map<const Buffer<Scalar>>(pairs.a.data(), pairs.size()); | |
| check_scalar_fallback_common<Scalar>(pairs, in); | |
| check_binary<Scalar, op_cmp_ne>( | |
| pairs, [](Scalar x, Scalar y) { return x != y ? Eigen::internal::ptrue(x) : Eigen::internal::pzero(x); }, bits); | |
| check_binary<Scalar, op_cmp_gt>( | |
| pairs, [](Scalar x, Scalar y) { return x > y ? Eigen::internal::ptrue(x) : Eigen::internal::pzero(x); }, bits); | |
| check_binary<Scalar, op_cmp_ge>( | |
| pairs, [](Scalar x, Scalar y) { return x >= y ? Eigen::internal::ptrue(x) : Eigen::internal::pzero(x); }, bits); | |
| check_unary<Scalar, op_pnegate>(in, test::negate<Scalar>, bits); | |
| check_unary<Scalar, op_pabs>( | |
| in, [](Scalar x) { return Eigen::internal::pabs(x); }, bits); | |
| check_binary<Scalar, op_psub>(pairs, test::REF_SUB<Scalar>, bits); | |
| check_binary<Scalar, op_pdiv>(pairs, test::REF_DIV<Scalar>, bits); | |
| // A one-ulp sqrtf error can cross a bfloat16 rounding boundary. | |
| check_unary<Scalar, op_psqrt>( | |
| in, [](Scalar x) { return Eigen::internal::psqrt(x); }, compare_ulps<Scalar>{kSqrtFloatUlps}); | |
| check_unary<Scalar, op_pfloor>( | |
| in, [](Scalar x) { return Eigen::internal::pfloor(x); }, bits); | |
| } | |
| template <typename Scalar> | |
| void packetmath_gpu_preinterpret() { | |
| using Bits = typename Eigen::numext::get_integer_by_size<sizeof(Scalar)>::signed_type; | |
| const int kSize = packet_layout<typename packet_traits<Scalar>::type>::kSize; | |
| const Buffer<Scalar> in = unary_inputs<Scalar>(kSize, 1 << 10); | |
| Buffer<Bits> out(in.size()); | |
| out.setZero(); | |
| run_on_gpu(preinterpret_scalar_kernel<Scalar, Bits>(), int(in.size()), in, out); | |
| for (Index k = 0; k < in.size(); ++k) { | |
| VERIFY_IS_EQUAL(out[k], Eigen::numext::bit_cast<Bits>(in[k])); | |
| } | |
| } | |
| // Compiled in the host pass as well as the device pass: with EIGEN_USE_GPU defined, packet_traits<float>::type is | |
| // float4 in both, so an expression reaching psign, a comparison or a bit operation has to compile on the host too. | |
| // It did not before those operations moved out of the device-only block. | |
| void host_pass_instantiation() { | |
| Eigen::Array<float, 32, 1> a = Eigen::Array<float, 32, 1>::Random(); | |
| a(0) = std::numeric_limits<float>::quiet_NaN(); | |
| const Eigen::Array<float, 32, 1> s = a.sign(); | |
| const Eigen::Array<bool, 32, 1> nans = a.isNaN(); | |
| const Eigen::Array<float, 32, 1> clamped = a.cwiseMax(0.0f).cwiseMin(1.0f); | |
| VERIFY((Eigen::numext::isnan)(s(0))); | |
| VERIFY(nans(0)); | |
| VERIFY(clamped(1) >= 0.0f && clamped(1) <= 1.0f); | |
| } | |
| } // namespace packetmath_gpu_test | |
| using namespace packetmath_gpu_test; | |
| EIGEN_DECLARE_TEST(packetmath_gpu) { | |
| ei_test_init_gpu(); | |
| CALL_SUBTEST_1(host_pass_instantiation()); | |
| CALL_SUBTEST_1(packetmath_gpu_real_core<float>()); | |
| CALL_SUBTEST_2(packetmath_gpu_real_math<float>()); | |
| CALL_SUBTEST_3(packetmath_gpu_real_core<double>()); | |
| CALL_SUBTEST_4(packetmath_gpu_real_math<double>()); | |
| CALL_SUBTEST_5(packetmath_gpu_half_core()); | |
| CALL_SUBTEST_6(packetmath_gpu_half_math()); | |
| CALL_SUBTEST_7(packetmath_gpu_integer_fallback<int32_t>()); | |
| CALL_SUBTEST_7(packetmath_gpu_integer_fallback<int64_t>()); | |
| CALL_SUBTEST_7(packetmath_gpu_integer_fallback<uint8_t>()); | |
| CALL_SUBTEST_7(packetmath_gpu_bool_fallback()); | |
| CALL_SUBTEST_7(packetmath_gpu_bfloat16_fallback()); | |
| CALL_SUBTEST_7(packetmath_gpu_preinterpret<float>()); | |
| CALL_SUBTEST_7(packetmath_gpu_preinterpret<double>()); | |
| } |