/* * Copyright (c) Advanced Micro Devices, Inc., or its affiliates. * * SPDX-License-Identifier: MIT */ #pragma once #define HIP_ENABLE_WARP_SYNC_BUILTINS #define HIP_ENABLE_EXTRA_WARP_SYNC_TYPES #include #include #include #include #include #include #include #include #include #include #define MASK_SHIFT(x, n) \ (x & (static_cast(1) << n)) >> n const unsigned long long Every5thBit = 0x1084210842108421; const unsigned long long Every9thBit = 0x8040201008040201; const unsigned long long Every5thBut9th = Every5thBit & ~Every9thBit; const unsigned long long AllThreads = ~0; // number of warps to reduce. Both when testing warp intrinsics and cooperative groups // must be a multiple of warpSize static constexpr int kNumReduces = 78 * 32; inline __device__ bool deactivate_thread(const uint64_t* const active_masks) { const auto warp = cooperative_groups::tiled_partition(cooperative_groups::this_thread_block(), warpSize); const auto block = cooperative_groups::this_thread_block(); const auto warps_per_block = (block.size() + warpSize - 1) / warpSize; const auto block_rank = (blockIdx.z * gridDim.y + blockIdx.y) * gridDim.x + blockIdx.x; const auto idx = block_rank * warps_per_block + block.thread_rank() / warpSize; return !(active_masks[idx] & (static_cast(1) << warp.thread_rank())); } inline std::mt19937& GetRandomGen() { static std::mt19937 mt(std::random_device{}()); return mt; } template inline T GenRandomInteger(const T min, const T max) { std::uniform_int_distribution dist(min, max); return dist(GetRandomGen()); } template inline T GenRandomReal(const T min, const T max) { std::uniform_real_distribution dist(min, max); return dist(GetRandomGen()); } inline int generate_width(int warp_size) { int exponent = 0; while (warp_size >>= 1) { ++exponent; } return GENERATE_COPY(map([](int e) { return 1 << e; }, range(1, exponent + 1))); } inline uint64_t get_active_mask(unsigned int warp_id, unsigned int warp_size) { uint64_t active_mask = 0; switch (warp_id % 5) { case 0: // even threads in the warp active_mask = 0xAAAAAAAAAAAAAAAA; break; case 1: // odd threads in the warp active_mask = 0x5555555555555555; break; case 2: // first half of the warp for (int i = 0; i < warp_size / 2; i++) { active_mask = active_mask | (static_cast(1) << i); } break; case 3: // second half of the warp for (int i = warp_size / 2; i < warp_size; i++) { active_mask = active_mask | (static_cast(1) << i); } break; case 4: // all threads active_mask = 0xFFFFFFFFFFFFFFFF; break; } return active_mask; } template ::value, bool> = true> inline T expandPrecision(int X) { return X; } template ::value, bool> = true> inline T expandPrecision(int X) { return X * 3.141592653589793115997963468544185161590576171875; } template ::value, bool> = true> inline __half expandPrecision(int X) { return (__half)expandPrecision(X); } template ::value, bool> = true> inline __half2 expandPrecision(int X) { __half H = expandPrecision(X); return {H, H}; } template ::value, bool> = true> inline void expandPrecision(T* Array, int size) { (void)Array; (void)size; } template ::value, bool> = true> inline void expandPrecision(T *Array, int size) { for (int i = 0; i != size; ++i) { Array[i] *= 3.141592653589793115997963468544185161590576171875; } } template inline void initializeInput(T *Input, int size) { int Values[] = {0, -1, 2, 3, 4, 5, -6, 7, 8, -9, 10, 11, 12, 13, -14, 15, 16, 17, -18, 19, 20, -21, 22, 23, 24, 25, 26, -27, 28, 29, 30, 31, -32, 33, 34, 35, -36, 37, 38, -39, 40, 41, 42, 43, -44, -45, 46, 47, 48, 49, 50, -51, 52, 53, -54, 55, 56, 57, -58, 59, 60, 61, 62, -63}; for (int i = 0; i != size; ++i) { Input[i] = expandPrecision(Values[i]); } } template inline void initializeExpected(T *Expected, int *Values, int size) { for (int i = 0; i != size; ++i) { Expected[i] = expandPrecision(Values[i]); } } template inline bool compareEqual(T X, T Y) { return X == Y; } template <> inline bool compareEqual(__half X, __half Y) { return __half2float(X) == __half2float(Y); } template <> inline bool compareEqual(__half2 X, __half2 Y) { return compareEqual(X.x, Y.x) && compareEqual(X.y, Y.y); } inline bool compareMaskEqual(unsigned long long *Actual, unsigned long long *Expected, int i, int warpSize) { if (warpSize == 32) return (unsigned)Actual[i] == (unsigned)Expected[i]; return Actual[i] == Expected[i]; } template inline T alignUp(T num, size_t n) { if (num % n == 0) { return num; } return ((num + n - 1) / n) * n; } template struct DistributionType { using type = std::uniform_int_distribution; }; // there is no std::uniform_real_distribution for 'half' type, so we cast from // unsigned short, avoiding Nan and Infinity template <> struct DistributionType<__half> { using type = std::uniform_int_distribution; }; template <> struct DistributionType { using type = std::uniform_real_distribution; }; template <> struct DistributionType { using type = std::uniform_real_distribution; }; template struct MinOp { T operator()(T lhs, T rhs) const { return std::min(lhs, rhs); } }; template struct MaxOp { T operator()(T lhs, T rhs) const { return std::max(lhs, rhs); } }; template struct XorOp { __host__ __device__ T operator()(T lhs, T rhs) { return lhs ^ rhs; } }; template struct AndOp { __host__ __device__ T operator()(T lhs, T rhs) { return lhs & rhs; } }; template struct OrOp { __host__ __device__ T operator()(T lhs, T rhs) { return lhs | rhs; } }; template struct MaxOfAbsolute { T __host__ __device__ operator()(T i, T j) { return std::max(std::abs(i), std::abs(j)); } }; template struct NonCommutativeOp { T __host__ __device__ operator()(T i, T j) { return std::abs(i) * j; } }; // typeid(T).name() does seem to return a very descriptive name for primitive types, // at least on clang, so we roll out an equivalent template const char* typeToString() { if (std::is_same::value) return "int"; if (std::is_same::value) return "unsigned int"; if (std::is_same::value) return "long long"; if (std::is_same::value) return "unsigned long long"; if (std::is_same::value) return "half"; if (std::is_same::value) return "float"; if (std::is_same::value) return "double"; assert(false && "Missing conversion to string for type"); return ""; } template const char* opToString() { if constexpr (std::is_same>::value) return "add"; else if constexpr (std::is_same>::value) return "min"; else if constexpr (std::is_same>::value) return "max"; else if constexpr (std::is_same>::value) return "logical_and"; else if constexpr (std::is_same>::value) return "logical_or"; else if constexpr (std::is_same>::value) return "logical_xor"; #if HT_AMD else if constexpr (std::is_same>::value) return "cooperative_groups::plus"; else if constexpr (std::is_same>::value) return "cooperative_groups::less"; else if constexpr (std::is_same>::value) return "cooperative_groups::greater"; else if constexpr (std::is_same>::value) return "cooperative_groups::bit_and"; else if constexpr (std::is_same>::value) return "cooperative_groups::bit_or"; else if constexpr (std::is_same>::value) return "cooperative_groups::bit_xor"; #endif else if constexpr (std::is_same>::value) return "MaxOfAbsolute"; else if constexpr (std::is_same>::value) return "NonCommutativeOp"; else { return "unknown operator"; } } template void genRandomMasks(LinearAllocGuard& d_buf, LinearAllocGuard& buf, Gen& gen, int numItems) { // masks must be != 0, hence passing 1 as the 'a' distribution parameter int wavefrontSize = getWarpSize(); std::uniform_int_distribution dist(1, wavefrontSize == 64? ~0ull : (1ull << 32) - 1); std::uniform_int_distribution distNoHoles(1, getWarpSize() - 2); int numBytes = numItems * sizeof(T); LinearAllocGuard tmp(LinearAllocs::malloc, numBytes); LinearAllocGuard d_tmp(LinearAllocs::hipMalloc, numBytes); buf = std::move(tmp); d_buf = std::move(d_tmp); for (int i = 0; i < numItems; i++) { T mask; if (i % 5 == 0) { // every five masks, create a mask that starts in position zero and has "no holes", // because those take a different code path, where DPP instructions are used mask = 1ull << distNoHoles(gen); mask--; } else { mask = dist(gen); } buf.ptr()[i] = mask; } HIP_CHECK(hipMemcpy(d_buf.ptr(), buf.ptr(), numBytes, hipMemcpyHostToDevice)); } // generates a random __half (instead of using uniform_real_distribution casting to __half // which is problematic) template __half genRandomHalf(std::uniform_int_distribution& dist, Gen& gen, unsigned char exponent) { __half_raw tmp; unsigned short mantissa = dist(gen) & 0x7ff; tmp.x = (exponent << 10) | mantissa; return tmp; } // generates a random buffer in buf, copies it to device memory in d_buf template void genRandomBuffers(LinearAllocGuard& d_buf, LinearAllocGuard& buf, Dist& dist, Gen& gen, int numItems) { int numBytes = numItems * sizeof(T); LinearAllocGuard tmp(LinearAllocs::malloc, numBytes); LinearAllocGuard d_tmp(LinearAllocs::hipMalloc, numBytes); buf = std::move(tmp); d_buf = std::move(d_tmp); if constexpr (std::is_same::value) { // avoid denormals (exponent = 0) and NaN/infinite (exponent = 255). // use numbers to be (-8> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v |= v >> 32; return ++v; } // Sets id to the expected value exclusive_scan returns for the first active lane template void scanIdentity(T& id) { T result = {}; if constexpr (std::is_same>::value || std::is_same>::value) { if constexpr (std::is_same::value) { result = HIPRT_INF_FP16; } else if (std::is_floating_point::value) { result = std::numeric_limits::infinity(); } else { result = std::numeric_limits::max(); } } else if constexpr (std::is_same>::value || std::is_same>::value) { if constexpr (std::is_same::value) { result = -HIPRT_INF_FP16; } else if (std::is_floating_point::value) { result = -std::numeric_limits::infinity(); } else { result = std::numeric_limits::lowest(); } } else if constexpr (std::is_same>::value) { result = ~result; } else if constexpr (std::is_same>::value) { } else { std::memset(&result, 0, sizeof(T)); } id = result; } // given an operation produces the expected result of the warp-wide reduction // @mask indicates the lanes that will participate in the computation // @return the result associated the lane with the highest index that is active according to the // mask template T calculateExpected(T* output, const T* input, Op op, unsigned long long mask, AggregationType aggType) { T result; bool inclusive = aggType != AggregationType::ExclusiveScan && aggType != AggregationType::ExclusiveScanDefault; int lastLane = 64 - __builtin_clzll(mask) - 1; T aggregation[64]; // the results for the previous step of the aggregation T lastAggregation[64]; // identity value for this type and operator T id {}; std::memset(aggregation, 0, 64 * sizeof(T)); std::memset(lastAggregation, 0, 64 * sizeof(T)); scanIdentity(id); for (int i = 0; i < 64; i++) { output[i] = id; } if constexpr (std::is_same>::value || std::is_same>::value ) { for (int i = 0; i < lastLane + 1; i++) { if (mask & (1ull << i)) { aggregation[i] = input[i]; lastAggregation[i] = input[i]; } } for (int modulo = 2; modulo <= nextPowerOf2(lastLane + 1); modulo *= 2) { for (int i = 0; i < lastLane + 1; i += 1) { int j = i - modulo / 2; if (j >= 0) { aggregation[i] += lastAggregation[j]; } } std::memcpy(lastAggregation, aggregation, sizeof(lastAggregation)); } for (int i = 0; i < lastLane + 1; i += 1) { if (inclusive) { output[i] = aggregation[i]; } else if (i > 0) { output[i] = aggregation[i - 1]; } } result = output[lastLane]; } else if constexpr (std::is_same>::value) { MinOp minOp; return calculateExpected(output, input, minOp, mask, aggType); } else if constexpr (std::is_same>::value) { MaxOp maxOp; return calculateExpected(output, input, maxOp, mask, aggType); } else if constexpr (std::is_same>::value) { std::bit_xor xorOp; return calculateExpected(output, input, xorOp, mask, aggType); } else if constexpr (std::is_same>::value) { std::bit_or orOp; return calculateExpected(output, input, orOp, mask, aggType); } else if constexpr (std::is_same>::value) { std::bit_and andOp; return calculateExpected(output, input, andOp, mask, aggType); } else { bool initialized = false; result = id; for (int i = 0; i < lastLane + 1; i++) { if (mask & (1ull << i)) { if (initialized) { if (inclusive) { result = op(result, input[i]); output[i] = result; } else { output[i] = result; result = op(result, input[i]); } } else { result = input[i]; if (inclusive) { output[i] = result; } else { output[i] = id; } initialized = true; } } } } if (aggType == AggregationType::Reduce) { for (int i = 0; i < lastLane + 1; i++) { output[i] = result; } } return result; } template void printMismatch(const T& result, const T& expected, const T* input, unsigned long long mask, int laneId) { std::ios init(NULL); init.copyfmt(std::cout); std::cout << "\nMismatch at lane: " << laneId << "\n"; std::cout << "Mask: 0x" << std::hex << std::setfill('0') << std::setw(16) << mask << "\n"; std::cout << "Input:\n"; std::cout.copyfmt(init); for (int i = 0; i < getWarpSize(); i++) { if ((1ull << i) & mask) { if constexpr (std::is_same::value) { const unsigned char* ptr = reinterpret_cast(&input[i]); std::cout << "Lane " << i << ": " << __half2float(input[i]) << std::hex << " (0x" << static_cast(ptr[1]) << static_cast(ptr[0]) << ")" << "\n"; std::cout.copyfmt(init); } else { std::cout << "Lane " << i << ": " << input[i] << "\n"; } } } if constexpr (std::is_same::value) { std::cout << "Result: " << __half2float(result) << "\n"; std::cout << "Expected: " << __half2float(expected) << "\n"; } else { std::cout << "Result: " << result << "\n"; std::cout << "Expected: " << expected << "\n"; } } template void compareFloatingPoint(const T& result, const T& expected, unsigned long long mask, const T* input, int laneId) { using namespace Catch::Matchers; std::string opName = opToString(); if constexpr (std::is_same::value) { float resultFloat = __half2float(result); float expectedFloat = __half2float(expected); float absDifference = fabs(resultFloat - expectedFloat); float relativeEpsilon = 0.1 * fmax(resultFloat, expectedFloat); float eps = 0.01f; if constexpr (std::is_same::value) { INFO("result: 0x" << std::hex << __half_as_ushort(result)); } else { INFO("result: 0x" << std::hex << result); } REQUIRE(!__hisnan(result)); if (relativeEpsilon > eps) { if (absDifference > 0.0001) { if (absDifference >= eps * fabs(fmax(resultFloat, expectedFloat))) { printMismatch(result, expected, input, mask, laneId); std::cout << "Relative epsilon: " << relativeEpsilon << "\n"; std::cout << "Difference: " << absDifference << "\n"; } } INFO("Operator: " << opName << " mask: 0x" << std::hex << mask); REQUIRE_THAT(__half2float(resultFloat), WithinRel(expectedFloat, eps)); } } else { // for float or double, also lossy in terms of precision T absDifference = fabs(result - expected); T relativeEpsilon = 0.1 * fmax(result, expected); T eps = 0.01; if (relativeEpsilon > eps) { if (absDifference > 0.0001) { if (absDifference >= eps * fabs(fmax(result, expected))) { printMismatch(result, expected, input, mask, laneId); std::cout << "Relative epsilon: " << relativeEpsilon << "\n"; std::cout << "Difference: " << absDifference << "\n"; } INFO("Operator: " << opName << " mask: 0x" << std::hex << mask); REQUIRE_THAT(result, WithinRel(expected, eps)); } } } } // @tparam Reduce a functor; abstracts away kernel dispatching // (via hiprtc or normal execution) template class Op> void runTestReduce(int iteration, Reduce reduce) { using namespace Catch::Matchers; using distribution = typename DistributionType::type; unsigned int wavefrontSize = getWarpSize(); // one result per reduce per thread to be checked LinearAllocGuard d_output(LinearAllocs::hipMalloc, kNumReduces * wavefrontSize * sizeof(T)); LinearAllocGuard output(LinearAllocs::malloc, kNumReduces * wavefrontSize * sizeof(T)); std::mt19937_64 gen(iteration); // for float16, we generate any random unsigned short, but cap the exponent later on // On the rest of the types, just use a bigger reduced range of numbers to avoid overflows too typename distribution::result_type a = std::is_same::value? std::numeric_limits::lowest() : (std::is_signed::value? -1023 : 0); typename distribution::result_type b = std::is_same::value? std::numeric_limits::max() : 1023; distribution dist(a, b); LinearAllocGuard input, d_input; LinearAllocGuard masks, d_masks; Op op; std::string opName = opToString>(); int numReduce = 0; genRandomBuffers(d_input, input, dist, gen, kNumReduces * wavefrontSize); genRandomMasks(d_masks, masks, gen, kNumReduces); reduce(d_output.ptr(), d_input.ptr(), d_masks.ptr(), kNumReduces, op); HIP_CHECK(hipDeviceSynchronize()); HIP_CHECK(hipMemcpy(output.ptr(), d_output.ptr(), d_output.size_bytes(), hipMemcpyDeviceToHost)); while (numReduce < kNumReduces) { T expectedByLane[64]; T* waveInput = &input.ptr()[numReduce * wavefrontSize]; T expected = calculateExpected(expectedByLane, waveInput, op, masks.ptr()[numReduce], AggregationType::Reduce); int lane = 0; while (lane < wavefrontSize) { auto result = output.ptr()[numReduce * wavefrontSize + lane]; unsigned long long mask = masks.ptr()[numReduce]; if ((1ull << lane) & mask) { if constexpr (std::is_integral::value || std::is_same, MinOp>::value || std::is_same, MaxOp>::value) { // for integral types or min/max the result should match exactly if constexpr (std::is_same::value) REQUIRE(__half2float(result) == __half2float(expected)); else { if (result != expected) { printMismatch(result, expected, waveInput, mask, lane); INFO("Operator: " << opName << " mask: 0x" << std::hex << mask); REQUIRE(result == expected); } } } else compareFloatingPoint>(result, expected, mask, waveInput, lane); } lane++; } numReduce++; } }