/******************************************************************************* * * MIT License * * Copyright (C) 2021-2025 Advanced Micro Devices, Inc. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * *******************************************************************************/ #ifndef ROCWMMA_TEST_COMMON_HPP #define ROCWMMA_TEST_COMMON_HPP #if ROCWMMA_TESTS_NO_HALF #warning("Building tests with hfloat16_t requires !HIP_NO_HALF && !__HIP_NO_HALF_CONVERSIONS__. Proceeding without hfloat16_t") #endif // !ROCWMMA_NO_HALF && __HIP_NO_HALF_CONVERSIONS__ #include #include #include #include #include #include #include "test_config.hpp" #include "device/common.hpp" #ifndef CHECK_HIP_ERROR #define CHECK_HIP_ERROR(expression) \ if(auto status = (expression); status != hipSuccess) \ { \ fprintf(stderr, \ "hip error: '%s'(%d) at %s:%d\n", \ hipGetErrorString(status), \ status, \ __FILE__, \ __LINE__); \ exit(EXIT_FAILURE); \ } #endif #if ROCWMMA_BENCHMARK_TESTS #ifndef CHECK_AMDSMI_ERROR #define CHECK_AMDSMI_ERROR(expression, smiErrorFlag) \ if(auto status = (expression); status != AMDSMI_STATUS_SUCCESS) \ { \ const char* errName; \ amdsmi_status_code_to_string(status, &errName); \ fprintf(stderr, "amdsmi error: '%s'(%d) at %s:%d\n", errName, status, __FILE__, __LINE__); \ smiErrorFlag = true; \ } #endif #endif // ROCWMMA_BENCHMARK_TESTS namespace rocwmma { ROCWMMA_DEVICE inline bool isFirstThread() { return (threadIdx.x == 0) && (threadIdx.y == 0) && (threadIdx.z == 0) && (blockIdx.x == 0) && (blockIdx.y == 0) && (blockIdx.z == 0); } template ROCWMMA_DEVICE bool expectEqual(Lhs&& testL, Rhs&& testR, bool debugOnFail, const char* file, uint32_t line) { bool compare_result = (testL == testR); if(!compare_result && debugOnFail && isFirstThread()) { printf("\n"); printf("File: %s L:%d\n", file, line); if constexpr(is_same_v, hfloat16_t>) { printf("Lhs: %f, Rhs: %f\n", static_cast(testL), static_cast(testR)); } printf("FAILED"); printf("\n"); } return compare_result; } template ROCWMMA_DEVICE bool expectNotEqual(Lhs&& testL, Rhs&& testR, bool debugOnFail, const char* file, uint32_t line) { bool compare_result = (testL != testR); if(!compare_result && debugOnFail && isFirstThread()) { printf("\n"); printf("File: %s L:%d\n", file, line); printf("FAILED"); printf("\n"); } return compare_result; } static constexpr uint32_t ERROR_VALUE = 7u; static constexpr uint32_t SUCCESS_VALUE = 0u; namespace quirks { // rocBLAS does not yet support Ti/To/Tc = bf16/bf16/bf16 template struct rocblas_supported : std::true_type { }; template <> struct rocblas_supported : std::false_type { }; template <> struct rocblas_supported : std::false_type { }; #if !defined(ROCBLAS_DATA_TYPE_FLOAT8) template <> struct rocblas_supported : std::false_type { }; template <> struct rocblas_supported : std::false_type { }; #endif template <> struct rocblas_supported : std::false_type { }; template <> struct rocblas_supported : std::false_type { }; } // namespace quirks template struct MatrixUtil { template __host__ static inline void print(DataT const* mat, uint32_t m, uint32_t n, std::ostream& stream = std::cout) { auto rowMjr = [](uint32_t row, uint32_t col, uint32_t ld) { return row * ld + col; }; auto colMjr = [](uint32_t row, uint32_t col, uint32_t ld) { return col * ld + row; }; auto index = std::is_same::value ? rowMjr : colMjr; auto ld = std::is_same::value ? n : m; for(int i = 0; i < m; ++i) // row { stream << "[ "; for(int j = 0; j < n; ++j) // col { // (Row, col) stream << mat[index(i, j, ld)] << " "; } stream << "]\n"; } stream << "\n"; } template __host__ static inline void print(std::vector const& mat, uint32_t m, uint32_t n, std::ostream& stream = std::cout) { assert(mat.size() == n * m); print(mat.data(), m, n, stream); } template __host__ static inline void printAsType(DataT const* mat, uint32_t m, uint32_t n, std::ostream& stream = std::cout) { auto rowMjr = [](uint32_t row, uint32_t col, uint32_t ld) { return row * ld + col; }; auto colMjr = [](uint32_t row, uint32_t col, uint32_t ld) { return col * ld + row; }; auto index = std::is_same::value ? rowMjr : colMjr; auto ld = std::is_same::value ? n : m; for(int i = 0; i < m; ++i) // row { stream << "[ "; for(int j = 0; j < n; ++j) // col { // (Row, col) stream << static_cast(mat[index(i, j, ld)]) << " "; } stream << "]\n"; } stream << "\n"; } template __host__ static inline void fillWithPadding( DataT* mat, uint32_t m, uint32_t n, uint32_t padM, uint32_t padN, DataT padValue) { auto rowMjr = [](uint32_t row, uint32_t col, uint32_t ld) { return row * ld + col; }; auto colMjr = [](uint32_t row, uint32_t col, uint32_t ld) { return col * ld + row; }; const auto limitM = m + 2 * padM; const auto limitN = n + 2 * padN; auto index = std::is_same::value ? rowMjr : colMjr; auto ld = std::is_same::value ? limitN : limitM; #pragma omp parallel for for(int i = 0; i < limitM; ++i) // row { #pragma omp parallel for for(int j = 0; j < limitN; ++j) // col { auto idx = index(i, j, ld); if(i < padM || i >= (limitM - padM) || j < padN || j >= (limitN - padN)) { mat[idx] = padValue; } else { // Count up in integers, in ascending order for each row. auto value = ((i - padM) * n + (j - padN)) % 5; mat[idx] = ((value % 3) && std::is_signed::value) ? -static_cast(value) : static_cast(value); } } } } template __host__ static inline void fillWithPadding(std::vector& mat, uint32_t m, uint32_t n, uint32_t padM, uint32_t padN, DataT padValue) { assert(mat.size() == n * m); fillWithPadding(mat.data(), m, n, padM, padN, padValue); } template __host__ static inline void fillWithPaddingLaunchKernel( DataT* d_mat, uint32_t m, uint32_t n, uint32_t padM, uint32_t padN, DataT padValue) { const auto limitM = m + 2 * padM; const auto limitN = n + 2 * padN; auto blockDim = dim3(1024, 1, 1); auto gridDim = dim3(ceil_div(limitM * limitN, blockDim.x), 1, 1); hipLaunchKernelGGL((fillWithPaddingKernel), gridDim, blockDim, 0, 0, d_mat, m, n, padM, padN, padValue); } template __host__ static inline void fill(DataT* mat, uint32_t m, uint32_t n) { auto rowMjr = [](uint32_t row, uint32_t col, uint32_t ld) { return row * ld + col; }; auto colMjr = [](uint32_t row, uint32_t col, uint32_t ld) { return col * ld + row; }; auto index = std::is_same::value ? rowMjr : colMjr; auto ld = std::is_same::value ? n : m; #pragma omp parallel for for(int i = 0; i < m; ++i) // row { #pragma omp parallel for for(int j = 0; j < n; ++j) // col { // Count up in integers, in ascending order for each row. auto value = (i * n + j) % 5; auto idx = index(i, j, ld); mat[idx] = ((value % 3) && std::is_signed::value) ? -static_cast(value) : static_cast(value); } } } template __host__ static inline void fill(std::vector& mat, uint32_t m, uint32_t n) { assert(mat.size() == n * m); fill(mat.data(), m, n); } template __host__ static inline void fillVal(DataT* mat, uint32_t m, uint32_t n, DataT value) { #pragma omp parallel for for(int i = 0; i < m * n; ++i) // row { mat[i] = value; } } template __host__ static inline void fillVal(std::vector& mat, uint32_t m, uint32_t n, DataT value) { assert(mat.size() == n * m); fillVal(mat.data(), m, n, value); } // fill kernel wrapper for M x N matrix template __host__ static inline void fillLaunchKernel(DataT* d_mat, uint32_t m, uint32_t n) { auto blockDim = dim3(1024, 1, 1); auto gridDim = dim3(ceil_div(m * n, blockDim.x), 1, 1); hipLaunchKernelGGL((fillKernel), gridDim, blockDim, 0, 0, d_mat, m, n); } // fill kernel wrapper for batched M x K matrices template __host__ static inline void fillLaunchKernel(DataT* d_mat, uint32_t m, uint32_t k, uint32_t b) { auto blockDim = dim3(1024, 1, 1); auto gridDim = dim3(ceil_div(m * k, blockDim.x), 1, b); hipLaunchKernelGGL( (fillKernel), gridDim, blockDim, 0, 0, d_mat, m, k, b); } // fill kernel wrapper for M x N matrix for a specific value template __host__ static inline void fillValLaunchKernel(DataT* d_mat, uint32_t m, uint32_t n, DataT value) { auto blockDim = dim3(1024, 1, 1); auto gridDim = dim3(ceil_div(m * n, blockDim.x), 1, 1); hipLaunchKernelGGL( (fillValKernel), gridDim, blockDim, 0, 0, d_mat, m, n, value); } // fill kernel wrapper for M x N matrix for mat[i] = i template __host__ static inline void fillIdxLaunchKernel(DataT* d_mat, uint32_t m, uint32_t n) { auto blockDim = dim3(1024, 1, 1); auto gridDim = dim3(ceil_div(m * n, blockDim.x), 1, 1); hipLaunchKernelGGL( (fillIdxKernel), gridDim, blockDim, 0, 0, d_mat, m, n); } }; // compareEqual on two different layouts: must calculate index offsets template ::value, int> = 0> std::pair compareEqual(TypeA const* matrixA, TypeB const* matrixB, uint32_t m, uint32_t n, uint32_t lda, uint32_t ldb, double tolerance = 10.0) { bool retval = true; double max_relative_error = 0.0; // Some types don't have direct conversion to double. // Convert to float first then to double. auto toDoubleA = [](TypeA const& val) { return static_cast(static_cast(val)); }; auto toDoubleB = [](TypeB const& val) { return static_cast(static_cast(val)); }; auto rowMjr = [](uint32_t row, uint32_t col, uint32_t ld) { return row * ld + col; }; auto colMjr = [](uint32_t row, uint32_t col, uint32_t ld) { return col * ld + row; }; auto indexA = std::is_same::value ? rowMjr : colMjr; auto indexB = std::is_same::value ? rowMjr : colMjr; bool isInf = false; bool isNaN = false; std::mutex writeMutex; #pragma omp parallel for for(int i = 0; i < m; ++i) // Row { #pragma omp parallel for for(int j = 0; j < n; ++j) // Col { auto valA = matrixA[indexA(i, j, lda)]; auto valB = matrixB[indexB(i, j, ldb)]; auto numerator = fabs(toDoubleA(valA) - toDoubleB(valB)); auto divisor = fabs(toDoubleA(valA)) + fabs(toDoubleB(valB)) + 1.0; if(std::isinf(numerator) || std::isinf(divisor)) { #pragma omp atomic isInf |= true; } else { auto relative_error = numerator / divisor; if(std::isnan(relative_error)) { #pragma omp atomic isNaN |= true; } else if(relative_error > max_relative_error) { const std::lock_guard guard(writeMutex); // Double check in case of stall if(relative_error > max_relative_error) { max_relative_error = relative_error; } } } if(isInf || isNaN) { i = m; j = n; } } } auto eps = toDoubleA(std::numeric_limits::epsilon()); if(isInf) { retval = false; max_relative_error = std::numeric_limits::infinity(); } else if(isNaN) { retval = false; max_relative_error = double(std::numeric_limits::signaling_NaN()); } else if(max_relative_error > (eps * tolerance)) { retval = false; } return std::make_pair(retval, max_relative_error); } // compareEqual on two equal layouts: index offsets are identical // can use slightly faster 1D compare template ::value, int> = 0> std::pair compareEqual(TypeA const* matrixA, TypeB const* matrixB, uint32_t m, uint32_t n, uint32_t lda, uint32_t ldb, double tolerance = 10.0) { assert(lda == ldb && "Leading dims must match"); bool retval = true; double max_relative_error = 0.0; // Some types don't have direct conversion to double. // Convert to float first then to double. auto toDoubleA = [](TypeA const& val) { return static_cast(static_cast(val)); }; auto toDoubleB = [](TypeB const& val) { return static_cast(static_cast(val)); }; bool isInf = false; bool isNaN = false; std::mutex writeMutex; #pragma omp parallel for for(int i = 0; i < m * n; ++i) // Row { auto valA = matrixA[i]; auto valB = matrixB[i]; auto numerator = fabs(toDoubleA(valA) - toDoubleB(valB)); auto divisor = fabs(toDoubleA(valA)) + fabs(toDoubleB(valB)) + 1.0; if(std::isinf(numerator) || std::isinf(divisor)) { #pragma omp atomic isInf |= true; } else { auto relative_error = numerator / divisor; if(std::isnan(relative_error)) { #pragma omp atomic isNaN |= true; } else if(relative_error > max_relative_error) { const std::lock_guard guard(writeMutex); // Double check in case of stall if(relative_error > max_relative_error) { max_relative_error = relative_error; } } } if(isInf || isNaN) { i = m * n; } } auto eps = toDoubleA(std::numeric_limits::epsilon()); if(isInf) { retval = false; max_relative_error = std::numeric_limits::infinity(); } else if(isNaN) { retval = false; max_relative_error = double(std::numeric_limits::signaling_NaN()); } else if(max_relative_error > (eps * tolerance)) { retval = false; } return std::make_pair(retval, max_relative_error); } template inline std::pair compareEqual( TypeA const* matrixA, TypeB const* matrixB, uint32_t m, uint32_t n, double tolerance = 10.0) { uint32_t lda = std::is_same::value ? n : m; uint32_t ldb = std::is_same::value ? n : m; return compareEqual( matrixA, matrixB, m, n, lda, ldb, tolerance); } template inline std::pair compareEqual(std::vector const& a, std::vector const& b, uint32_t m, uint32_t n, double tolerance = 10.0) { assert(a.size() == b.size() && "A and B are not the same size"); assert(a.size() == m * n && "A and B do not match size M x N"); return compareEqual(a.data(), b.data(), m, n, tolerance); } template inline std::pair compareEqual(std::vector const& a, std::vector const& b, uint32_t m, uint32_t n, uint32_t lda, uint32_t ldb, double tolerance = 10.0) { assert(a.size() == b.size() && "A and B are not the same size"); assert(a.size() == m * n && "A and B do not match size m x n"); return compareEqual( a.data(), b.data(), m, n, lda, ldb, tolerance); } // compareEqual kernel wrapper for gemm tests template std::pair compareEqualLaunchKernel( TypeA* matrixA, TypeB* matrixB, uint32_t m, uint32_t n, double tolerance = 10.0) { uint32_t lda = std::is_same::value ? n : m; uint32_t ldb = std::is_same::value ? n : m; auto blockDim = dim3(1024, 1, 1); auto gridDim = dim3(ceil_div(m * n, blockDim.x), 1, 1); double* d_relativeError; double maxRelativeError; CHECK_HIP_ERROR(hipMalloc(&d_relativeError, m * n * sizeof(double))); hipEvent_t syncEvent; CHECK_HIP_ERROR(hipEventCreate(&syncEvent)); // Calculate the relative error for each element of matrix A/B hipLaunchKernelGGL((compareEqualKernel), gridDim, blockDim, 0, 0, matrixA, matrixB, d_relativeError, m, n, lda, ldb); CHECK_HIP_ERROR(hipEventRecord(syncEvent)); CHECK_HIP_ERROR(hipEventSynchronize(syncEvent)); // Determine the maximum relative error blockDim = dim3(512, 1, 1); uint32_t maxElements = 1024; uint32_t offset = 1; for(uint32_t i = m * n; i > 1; i = ceil_div(i, maxElements)) { gridDim = dim3(ceil_div(i, maxElements), 1, 1); auto elements = i > maxElements ? maxElements : i; hipLaunchKernelGGL((maxReduceKernel), gridDim, blockDim, 0, 0, d_relativeError, elements, offset, m * n); CHECK_HIP_ERROR(hipEventRecord(syncEvent)); CHECK_HIP_ERROR(hipEventSynchronize(syncEvent)); offset = offset * maxElements; } CHECK_HIP_ERROR( hipMemcpy(&maxRelativeError, d_relativeError, sizeof(double), hipMemcpyDeviceToHost)); CHECK_HIP_ERROR(hipEventDestroy(syncEvent)); // Free allocated device memory CHECK_HIP_ERROR(hipFree(d_relativeError)); bool retval = true; bool isNaN = std::isnan(maxRelativeError); auto toDoubleA = [](TypeA const& val) { return static_cast(static_cast(val)); }; auto eps = toDoubleA(std::numeric_limits::epsilon()); if(isNaN) { retval = false; maxRelativeError = double(std::numeric_limits::signaling_NaN()); } else if(maxRelativeError > (eps * tolerance)) { retval = false; } return std::make_pair(retval, maxRelativeError); } // compareEqual kernel wrapper for batched matrices template std::pair compareEqualLaunchKernel( TypeA* matrixA, TypeB* matrixB, uint32_t m, uint32_t k, uint32_t b, double tolerance = 10.0) { auto blockDim = dim3(1024, 1, 1); auto gridDim = dim3(ceil_div(m * k, blockDim.x), 1, b); double* d_relativeError; double maxRelativeError; CHECK_HIP_ERROR(hipMalloc(&d_relativeError, m * k * b * sizeof(double))); hipEvent_t syncEvent; CHECK_HIP_ERROR(hipEventCreate(&syncEvent)); // Calculate the relative error for each element of matrix A/B hipLaunchKernelGGL((compareEqualKernel), gridDim, blockDim, 0, 0, matrixA, matrixB, d_relativeError, m, k, b); CHECK_HIP_ERROR(hipEventRecord(syncEvent)); CHECK_HIP_ERROR(hipEventSynchronize(syncEvent)); // Determine the maximum relative error blockDim = dim3(512, 1, 1); uint32_t maxElements = 1024; uint32_t offset = 1; for(uint32_t i = m * k * b; i > 1; i = ceil_div(i, maxElements)) { gridDim = dim3(ceil_div(i, maxElements), 1, 1); auto elements = i > maxElements ? maxElements : i; hipLaunchKernelGGL((maxReduceKernel), gridDim, blockDim, 0, 0, d_relativeError, elements, offset, m * k * b); CHECK_HIP_ERROR(hipEventRecord(syncEvent)); CHECK_HIP_ERROR(hipEventSynchronize(syncEvent)); offset = offset * maxElements; } CHECK_HIP_ERROR( hipMemcpy(&maxRelativeError, d_relativeError, sizeof(double), hipMemcpyDeviceToHost)); CHECK_HIP_ERROR(hipEventDestroy(syncEvent)); // Free allocated device memory CHECK_HIP_ERROR(hipFree(d_relativeError)); bool retval = true; bool isNaN = std::isnan(maxRelativeError); auto toDoubleA = [](TypeA const& val) { return static_cast(static_cast(val)); }; auto eps = toDoubleA(std::numeric_limits::epsilon()); if(isNaN) { retval = false; maxRelativeError = double(std::numeric_limits::signaling_NaN()); } else if(maxRelativeError > (eps * tolerance)) { retval = false; } return std::make_pair(retval, maxRelativeError); } // Count occurrences of val in the input array template uint64_t countVal(DataT const* a, uint64_t size, DataT const& val, double tolerance = 10.0) { uint64_t count = 0; #pragma omp parallel for for(uint64_t i = 0; i < size; ++i) { using TestT = double; if(fabs(static_cast(val) - static_cast(a[i])) <= (tolerance * static_cast(std::numeric_limits::epsilon()))) { #pragma omp atomic ++count; } } return count; } // Count occurrences of val inside the rectangular padding of input matrix template __host__ static inline uint64_t countPaddingVal(DataT* mat, uint32_t m, uint32_t n, uint32_t padM, uint32_t padN, DataT padValue, double tolerance = 10.0) { auto rowMjr = [](uint32_t row, uint32_t col, uint32_t ld) { return row * ld + col; }; auto colMjr = [](uint32_t row, uint32_t col, uint32_t ld) { return col * ld + row; }; const auto limitM = m + 2 * padM; const auto limitN = n + 2 * padN; auto index = std::is_same::value ? rowMjr : colMjr; auto ld = std::is_same::value ? limitN : limitM; uint64_t count = 0; #pragma omp parallel for for(uint32_t i = 0; i < limitM; ++i) // row { #pragma omp parallel for for(uint32_t j = 0; j < limitN; ++j) // col { auto idx = index(i, j, ld); if(i < padM || i >= (limitM - padM) || j < padN || j >= (limitN - padN)) { using TestT = float64_t; if(fabs(static_cast(padValue) - static_cast(mat[idx])) <= (tolerance * static_cast(std::numeric_limits::epsilon()))) { #pragma omp atomic ++count; } } } } return count; } } // namespace rocwmma #endif // ROCWMMA_TEST_COMMON_HPP