/******************************************************************************* * * MIT License * * Copyright (c) 2025 Advanced Micro Devices, Inc. * * 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. * *******************************************************************************/ #include "gtest_common.hpp" #include #include #include "cpu_reduce_util.hpp" #include "workspace.hpp" // Recently added FP16, BFP16 and I8 reduce custom test cases fail compiler staging tests #define WORKAROUND_ISSUE_895 1 #if WORKAROUND_ISSUE_895 #define CAN_BE_DISABLED_HalfTest_reduce_custom_fp16 DISABLED_HalfTest_reduce_custom_fp16 #define CAN_BE_DISABLED_BHalfTest_reduce_custom_fp16 DISABLED_BHalfTest_reduce_custom_bfp16 #define CAN_BE_DISABLED_IntTest_reduce_custom_fp16 DISABLED_IntTest_reduce_custom_i8 #else #define CAN_BE_DISABLED_HalfTest_reduce_custom_fp16 HalfTest_reduce_custom_fp16 #define CAN_BE_DISABLED_BHalfTest_reduce_custom_fp16 BHalfTest_reduce_custom_bfp16 #define CAN_BE_DISABLED_IntTest_reduce_custom_fp16 IntTest_reduce_custom_i8 #endif namespace { using TestCase = std::tuple, std::vector, miopenReduceTensorOp_t, miopenNanPropagation_t, miopenReduceTensorIndices_t, std::vector>; template auto GetCpuImplResult(miopenDataType_t compTypeVal, const TVerifier& verifier) { if(compTypeVal == miopenFloat) { if(std::is_same::value) return verifier.template cpuImpl(); else return verifier.template cpuImpl(); } else if(compTypeVal == miopenHalf) { if(std::is_same::value) return verifier.template cpuImpl(); else if(std::is_same::value) return verifier.template cpuImpl(); else return verifier.template cpuImpl(); } else if(compTypeVal == miopenDouble) return verifier.template cpuImpl(); throw std::runtime_error("Type is not supported!"); } template struct verify_reduce_with_indices { miopen::ReduceTensorDescriptor reduce; tensor input; tensor output; tensor workspace; tensor indices; float alpha; float beta; miopenReduceTensorOp_t reduceOp; miopenDataType_t compTypeVal; miopenNanPropagation_t nanOpt; miopenReduceTensorIndices_t indicesOpt; miopenIndicesType_t indicesType; verify_reduce_with_indices(const miopen::ReduceTensorDescriptor& reduce_, const tensor& input_, const tensor& output_, const tensor& workspace_, const tensor& indices_, float alpha_, float beta_) : reduce(reduce_), input(input_), output(output_), workspace(workspace_), indices(indices_), alpha(alpha_), beta(beta_), reduceOp(reduce.reduceTensorOp_), compTypeVal(reduce.reduceTensorCompType_), nanOpt(reduce.reduceTensorNanOpt_), indicesOpt(reduce.reduceTensorIndices_), indicesType(reduce.reduceTensorIndicesType_) { } tensor cpu() const { using reduce::convert_type; const std::tuple, tensor> results = GetCpuImplResult(compTypeVal, *this); if(toVerifyData) { const auto& dimLengths = output.desc.GetLengths(); auto result_dataFloat = tensor(dimLengths); auto& result_dataT = std::get<0>(results); for(size_t i = 0; i < result_dataT.data.size(); i++) result_dataFloat.data[i] = convert_type(result_dataT.data[i]); return (result_dataFloat); } else { const auto& dimLengths = indices.desc.GetLengths(); auto result_indicesFloat = tensor(dimLengths); auto& result_indices = std::get<1>(results); for(size_t i = 0; i < result_indices.data.size(); i++) result_indicesFloat.data[i] = static_cast(result_indices.data[i]); return (result_indicesFloat); }; }; tensor gpu() const { using reduce::convert_type; std::tuple, tensor> results; results = gpuImpl(); if(toVerifyData) { const auto& dimLengths = output.desc.GetLengths(); auto result_dataFloat = tensor(dimLengths); tensor& result_dataT = std::get<0>(results); for(size_t i = 0; i < result_dataT.data.size(); i++) result_dataFloat.data[i] = convert_type(result_dataT.data[i]); return (result_dataFloat); } else { const auto& dimLengths = indices.desc.GetLengths(); auto result_indicesFloat = tensor(dimLengths); tensor& result_indices = std::get<1>(results); for(size_t i = 0; i < result_indices.data.size(); i++) result_indicesFloat.data[i] = static_cast(result_indices.data[i]); return (result_indicesFloat); }; }; template std::tuple, tensor> cpuImpl() const { auto inLengths = input.desc.GetLengths(); auto outLengths = output.desc.GetLengths(); auto inStrides = input.desc.GetStrides(); auto outStrides = output.desc.GetStrides(); // replicate auto res = output; auto res_indices = indices; std::vector invariantLengths; std::vector toReduceLengths; std::vector invariantDims; std::vector toReduceDims; for(int i = 0; i < inLengths.size(); i++) { if(inLengths[i] == outLengths[i]) invariantDims.push_back(i); else toReduceDims.push_back(i); } invariantLengths.resize(invariantDims.size()); std::transform(invariantDims.begin(), invariantDims.end(), invariantLengths.begin(), [&](int i) { return inLengths[i]; }); toReduceLengths.resize(toReduceDims.size()); std::transform(toReduceDims.begin(), toReduceDims.end(), toReduceLengths.begin(), [&](int i) { return inLengths[i]; }); bool reduceAllDims = invariantDims.empty(); auto opReduce = reduce::ReduceOpFn2(reduceOp); std::size_t divider = std::accumulate( toReduceLengths.begin(), toReduceLengths.end(), std::size_t{1}, std::multiplies<>{}); auto PreUnaryOp = reduce::PreUnaryOpFn(reduceOp, divider); if(reduceAllDims) { cpuImplReduceAllDims( inLengths, inStrides, PreUnaryOp, opReduce, res, res_indices); } else { cpuImplReduceFewDims(invariantLengths, toReduceLengths, inLengths, invariantDims, outStrides, toReduceDims, inStrides, PreUnaryOp, opReduce, res, res_indices); }; return (std::make_tuple(res, res_indices)); } template void cpuImplReduceFewDims(const std::vector& invariantLengths, const std::vector& toReduceLengths, const std::vector& inLengths, const std::vector& invariantDims, const std::vector& outStrides, const std::vector& toReduceDims, const std::vector& inStrides, auto& PreUnaryOp, auto& opReduce, tensor& res, tensor& res_indices) const { using reduce::convert_type; const auto indexes_1 = get_all_indexes(invariantLengths); const auto indexes_2 = get_all_indexes(toReduceLengths); // go through indexes of the invariant dimensions for(const auto& index_1 : indexes_1) { std::vector src_index; std::vector dst_index; src_index.resize(inLengths.size()); dst_index.resize(inLengths.size()); std::fill(dst_index.begin(), dst_index.end(), 0); for(int k = 0; k < invariantDims.size(); k++) dst_index[invariantDims[k]] = index_1[k]; auto dst_offset = get_offset_from_index(outStrides, dst_index); // generate the part of the index belonging to the invariant dims for(int k = 0; k < invariantDims.size(); k++) src_index[invariantDims[k]] = index_1[k]; compType accuVal = reduce::ReduceOpZeroVal(reduceOp); int accuIndex = 0; // go through indexes of the toReduce dimensions for(const auto& index_2 : indexes_2) { // generate the part of the index belonging to the toReduce dims for(int k = 0; k < toReduceDims.size(); k++) src_index[toReduceDims[k]] = index_2[k]; auto src_offset = get_offset_from_index(inStrides, src_index); auto currVal = convert_type(input.data[src_offset]); // unary operation before reducing, only needed by AMAX. For MIN/MAX, nothing is // actually done PreUnaryOp(currVal); auto currIndex = get_flatten_offset(toReduceLengths, index_2); reduce::binop_with_nan_check2( nanOpt, opReduce, accuVal, currVal, accuIndex, currIndex); }; // scale the accumulated value if(!reduce::float_equal_one(alpha)) accuVal *= convert_type(alpha); // scale the prior dst value and add it to the accumulated value if(!reduce::float_equal_zero(beta)) { accuVal += convert_type(output.data[dst_offset]) * convert_type(beta); } // store the reduced value to dst location res.data[dst_offset] = convert_type(accuVal); res_indices.data[dst_offset] = accuIndex; // store the index }; } template void cpuImplReduceAllDims(const std::vector& inLengths, const std::vector& inStrides, auto& PreUnaryOp, auto& opReduce, tensor& res, tensor& res_indices) const { using reduce::convert_type; const auto indexes_1 = get_all_indexes(inLengths); compType accuVal = reduce::ReduceOpZeroVal(reduceOp); int accuIndex = 0; // go through indexes of the invariant dimensions for(const auto& src_index : indexes_1) { auto src_offset = get_offset_from_index(inStrides, src_index); auto currVal = convert_type(input.data[src_offset]); // unary operation before reducing, only needed by AMAX. For MIN/MAX, nothing is // actually done PreUnaryOp(currVal); int currIndex = get_flatten_offset(inLengths, src_index); reduce::binop_with_nan_check2(nanOpt, opReduce, accuVal, currVal, accuIndex, currIndex); } // scale the accumulated value if(!reduce::float_equal_one(alpha)) accuVal *= convert_type(alpha); // scale the prior dst value and add it to the accumulated value if(!reduce::float_equal_zero(beta)) { accuVal += convert_type(output.data[0]) * convert_type(beta); } // store the reduced value to dst location res.data[0] = convert_type(accuVal); res_indices.data[0] = accuIndex; } std::tuple, tensor> gpuImpl() const { auto&& handle = get_handle(); auto input_dev = handle.Write(input.data); auto output_dev = handle.Write(output.data); // replicate auto res = output; auto res_indices = indices; Workspace idxspace{}; idxspace.Write(indices.data); Workspace wspace{}; wspace.Write(workspace.data); const double alpha64 = alpha; const double beta64 = beta; const void* const alphaPtr = (std::is_same::value) ? static_cast(&alpha64) : static_cast(&alpha); const void* const betaPtr = (std::is_same::value) ? static_cast(&beta64) : static_cast(&beta); if(wspace.size() > 0) { reduce.ReduceTensor(get_handle(), idxspace.ptr(), idxspace.size(), wspace.ptr(), wspace.size(), alphaPtr, input.desc, input_dev.get(), betaPtr, output.desc, output_dev.get()); } else { reduce.ReduceTensor(get_handle(), idxspace.ptr(), idxspace.size(), nullptr, 0, alphaPtr, input.desc, input_dev.get(), betaPtr, output.desc, output_dev.get()); }; res.data = handle.Read(output_dev, res.data.size()); res_indices.data = idxspace.Read(); return (std::make_tuple(res, res_indices)); } void fail() const { std::cout << "verify_reduce_with_indices failed" << std::endl; std::cout << "Input Tensor" << " " << input.desc.ToString() << std::endl; } }; template struct verify_reduce_no_indices { miopen::ReduceTensorDescriptor reduce; tensor input; tensor output; tensor workspace; float alpha; float beta; miopenReduceTensorOp_t reduceOp; miopenDataType_t compTypeVal; miopenNanPropagation_t nanOpt; verify_reduce_no_indices( // NOLINT (hicpp-member-init) const miopen::ReduceTensorDescriptor& reduce_, const tensor& input_, const tensor& output_, const tensor& workspace_, float alpha_, float beta_) : reduce(reduce_), input(input_), output(output_), workspace(workspace_), alpha(alpha_), beta(beta_), reduceOp(reduce_.reduceTensorOp_), compTypeVal(reduce_.reduceTensorCompType_), nanOpt(reduce_.reduceTensorNanOpt_) { } tensor cpu() const { using reduce::convert_type; const tensor result = GetCpuImplResult(compTypeVal, *this); const auto& dimLengths = output.desc.GetLengths(); auto result_dataFloat = tensor(dimLengths); for(size_t i = 0; i < result.data.size(); i++) result_dataFloat.data[i] = convert_type(result.data[i]); return (result_dataFloat); }; template tensor cpuImpl() const { auto inLengths = input.desc.GetLengths(); auto outLengths = output.desc.GetLengths(); auto inStrides = input.desc.GetStrides(); auto outStrides = output.desc.GetStrides(); // replicate auto res = output; std::vector invariantLengths; std::vector toReduceLengths; std::vector invariantDims; std::vector toReduceDims; for(int i = 0; i < inLengths.size(); i++) { if(inLengths[i] == outLengths[i]) invariantDims.push_back(i); else toReduceDims.push_back(i); } invariantLengths.resize(invariantDims.size()); for(int i = 0; i < invariantDims.size(); i++) invariantLengths[i] = inLengths[invariantDims[i]]; toReduceLengths.resize(toReduceDims.size()); for(int i = 0; i < toReduceDims.size(); i++) toReduceLengths[i] = inLengths[toReduceDims[i]]; bool reduceAllDims = invariantDims.empty(); auto opReduce = reduce::ReduceOpFn(reduceOp); std::size_t divider = std::accumulate( toReduceLengths.begin(), toReduceLengths.end(), std::size_t{1}, std::multiplies<>{}); auto PreUnaryOp = reduce::PreUnaryOpFn(reduceOp, divider); auto PosUnaryOp = reduce::PosUnaryOpFn(reduceOp, divider); if(reduceAllDims) { cpuImplReduceAllDims( inLengths, inStrides, PreUnaryOp, opReduce, PosUnaryOp, res); } else { cpuImplReduceFewDims(invariantLengths, toReduceLengths, inLengths, invariantDims, outStrides, toReduceDims, inStrides, PreUnaryOp, opReduce, PosUnaryOp, res); }; return (res); } template void cpuImplReduceFewDims(const std::vector& invariantLengths, const std::vector& toReduceLengths, const std::vector& inLengths, const std::vector& invariantDims, const std::vector& outStrides, const std::vector& toReduceDims, const std::vector& inStrides, auto& PreUnaryOp, auto& opReduce, auto& PosUnaryOp, tensor& res) const { using reduce::convert_type; const auto indexes_1 = get_all_indexes(invariantLengths); const auto indexes_2 = get_all_indexes(toReduceLengths); // go through indexes of the invariant dimensions for(const auto& index_1 : indexes_1) { std::vector src_index; std::vector dst_index; src_index.resize(inLengths.size()); dst_index.resize(inLengths.size()); std::fill(dst_index.begin(), dst_index.end(), 0); for(int k = 0; k < invariantDims.size(); k++) dst_index[invariantDims[k]] = index_1[k]; auto dst_offset = get_offset_from_index(outStrides, dst_index); // generate the part of the index belonging to the invariant dims for(int k = 0; k < invariantDims.size(); k++) src_index[invariantDims[k]] = index_1[k]; compType accuVal = reduce::ReduceOpZeroVal(reduceOp); // go through indexes of the toReduce dimensions for(const auto& index_2 : indexes_2) { // generate the part of the index belonging to the toReduce dims for(int k = 0; k < toReduceDims.size(); k++) src_index[toReduceDims[k]] = index_2[k]; auto src_offset = get_offset_from_index(inStrides, src_index); auto currVal = convert_type(input.data[src_offset]); PreUnaryOp(currVal); reduce::binop_with_nan_check(nanOpt, opReduce, accuVal, currVal); }; PosUnaryOp(accuVal); // scale the accumulated value if(!reduce::float_equal_one(alpha)) accuVal *= convert_type(alpha); // scale the prior dst value and add it to the accumulated value if(!reduce::float_equal_zero(beta)) { accuVal += convert_type(output.data[dst_offset]) * convert_type(beta); } // store the reduced value to dst location res.data[dst_offset] = convert_type(accuVal); } } template void cpuImplReduceAllDims(const std::vector& inLengths, const std::vector& inStrides, auto& PreUnaryOp, auto& opReduce, auto& PosUnaryOp, tensor& res) const { using reduce::convert_type; const auto indexes_1 = get_all_indexes(inLengths); compType accuVal = reduce::ReduceOpZeroVal(reduceOp); // go through indexes of the invariant dimensions for(const auto& src_index : indexes_1) { auto src_offset = get_offset_from_index(inStrides, src_index); auto currVal = convert_type(input.data[src_offset]); PreUnaryOp(currVal); reduce::binop_with_nan_check(nanOpt, opReduce, accuVal, currVal); }; PosUnaryOp(accuVal); // scale the accumulated value if(!reduce::float_equal_one(alpha)) accuVal *= convert_type(alpha); // scale the prior dst value and add it to the accumulated value if(!reduce::float_equal_zero(beta)) accuVal += convert_type(output.data[0]) * convert_type(beta); // store the reduced value to dst location res.data[0] = convert_type(accuVal); } tensor gpu() const { using reduce::convert_type; auto result = gpuImpl(); const auto& dimLengths = output.desc.GetLengths(); auto result_dataFloat = tensor(dimLengths); for(size_t i = 0; i < result.data.size(); i++) result_dataFloat.data[i] = convert_type(result.data[i]); return (result_dataFloat); }; tensor gpuImpl() const { auto&& handle = get_handle(); auto input_dev = handle.Write(input.data); auto output_dev = handle.Write(output.data); // replicate auto res = output; Workspace wspace{}; wspace.Write(workspace.data); const double alpha64 = alpha; const double beta64 = beta; const void* const alphaPtr = (std::is_same::value) ? static_cast(&alpha64) : static_cast(&alpha); const void* const betaPtr = (std::is_same::value) ? static_cast(&beta64) : static_cast(&beta); if(wspace.size() > 0) { reduce.ReduceTensor(get_handle(), nullptr, 0, wspace.ptr(), wspace.size(), alphaPtr, input.desc, input_dev.get(), betaPtr, output.desc, output_dev.get()); } else { reduce.ReduceTensor(get_handle(), nullptr, 0, nullptr, 0, alphaPtr, input.desc, input_dev.get(), betaPtr, output.desc, output_dev.get()); }; res.data = handle.Read(output_dev, res.data.size()); return (res); } void fail() const { std::cout << "verify_reduce_no_indices failed" << std::endl; std::cout << "Input Tensor" << " " << input.desc.ToString() << std::endl; } }; template std::vector> get_tensor_lengths() { if(std::is_same::value) { return { {4, 3, 60, 50}, }; } else { return { {64, 3, 280, 81}, }; } } std::vector> get_toreduce_dims() { std::vector> tensor_dims = { {0}, {1}, {2}, {3}, {0, 1}, {0, 3}, {0, 2}, {2, 3}, {0, 1, 3}, {1, 2, 3}, {0, 1, 2, 3}}; return tensor_dims; } template inline auto GenCasesFull() { std::vector> alphabetas = {{1.0f, 0.0f}, {0.5f, 0.5f}}; return testing::Combine(testing::ValuesIn(get_tensor_lengths()), testing::ValuesIn(get_toreduce_dims()), testing::Values(MIOPEN_REDUCE_TENSOR_ADD, MIOPEN_REDUCE_TENSOR_MUL, MIOPEN_REDUCE_TENSOR_AMAX, MIOPEN_REDUCE_TENSOR_AVG, MIOPEN_REDUCE_TENSOR_NORM1, MIOPEN_REDUCE_TENSOR_NORM2), testing::Values(0, 1), testing::Values(0, 1), testing::ValuesIn(alphabetas)); } template inline auto GetCasesFull() { static const auto cases = GenCasesFull(); return cases; } // Smoke (pre-commit) tier: a tiny representative subset -- a couple of // reduce-dimension layouts crossed with a few representative ops, no indices, // single alpha/beta. The complete cross product stays in the Full instantiation. template inline auto GenCasesSmoke() { std::vector> alphabetas = {{1.0f, 0.0f}}; // Reduce cases are expensive (each distinct op/dim compiles a kernel), so // keep Smoke minimal: canonical ADD op over a partial and a full reduction. // Other ops (AMAX/NORM2) are exercised by the Standard tier on every CI. return testing::Combine(testing::ValuesIn(get_tensor_lengths()), testing::ValuesIn(std::vector>{{0}, {0, 1, 2, 3}}), testing::Values(MIOPEN_REDUCE_TENSOR_ADD), testing::Values(0), testing::Values(0), testing::ValuesIn(alphabetas)); } // Standard (per-CI) tier: exercises every distinct reduce code path at least // once -- all reduce operators, the alpha/beta blend epilogue, and the AMAX // flattened-indices path -- without the full nanOpt x indices x alpha/beta cross // product (that stays in the Full instantiation). The kernel compile-cache key // (see src/reducetensor.cpp) covers reduceOp / nanOpt / indices / dim-count but // NOT alpha/beta, so the blend cases below are essentially free. template inline std::vector GenCasesStandard() { const auto lengths = get_tensor_lengths(); const auto dims = get_toreduce_dims(); const std::vector ops = {MIOPEN_REDUCE_TENSOR_ADD, MIOPEN_REDUCE_TENSOR_MUL, MIOPEN_REDUCE_TENSOR_AMAX, MIOPEN_REDUCE_TENSOR_AVG, MIOPEN_REDUCE_TENSOR_NORM1, MIOPEN_REDUCE_TENSOR_NORM2}; const std::vector ab_plain = {1.0f, 0.0f}; const std::vector ab_blend = {0.5f, 0.5f}; std::vector cases; // (1) Every operator x every reduce-dimension layout, no indices, no blend. for(const auto& len : lengths) for(const auto& op : ops) for(const auto& dim : dims) cases.emplace_back(len, dim, op, MIOPEN_NOT_PROPAGATE_NAN, MIOPEN_REDUCE_TENSOR_NO_INDICES, ab_plain); // (2) alpha/beta blend epilogue: every operator on one dim layout (free -- // alpha/beta is not part of the kernel compile key). for(const auto& len : lengths) for(const auto& op : ops) cases.emplace_back(len, dims.front(), op, MIOPEN_NOT_PROPAGATE_NAN, MIOPEN_REDUCE_TENSOR_NO_INDICES, ab_blend); // (3) Flattened-indices path: only AMAX produces indices, across every dim // layout. One nan-propagation case to touch that compile path too. for(const auto& len : lengths) { for(const auto& dim : dims) cases.emplace_back(len, dim, MIOPEN_REDUCE_TENSOR_AMAX, MIOPEN_NOT_PROPAGATE_NAN, MIOPEN_REDUCE_TENSOR_FLATTENED_INDICES, ab_plain); cases.emplace_back(len, dims.front(), MIOPEN_REDUCE_TENSOR_AMAX, MIOPEN_PROPAGATE_NAN, MIOPEN_REDUCE_TENSOR_FLATTENED_INDICES, ab_plain); } return cases; } template inline auto GetCasesSmoke() { static const auto cases = GenCasesSmoke(); return cases; } template inline auto GetCasesStandard() { static const auto cases = testing::ValuesIn(GenCasesStandard()); return cases; } inline auto GetCasesReduceCustomTestSet1() { static TestCase test_case = std::make_tuple(std::vector{1024, 30528, 1}, std::vector{0, 1, 2}, MIOPEN_REDUCE_TENSOR_ADD, MIOPEN_PROPAGATE_NAN, MIOPEN_REDUCE_TENSOR_NO_INDICES, std::vector{1.0f, 0.0f}); return testing::Values(test_case); } inline auto GetCasesReduceCustomTestSet2() { static std::vector alphabeta = {1.0f, 0.0f}; constexpr miopenReduceTensorOp_t add = MIOPEN_REDUCE_TENSOR_ADD; constexpr miopenReduceTensorOp_t mul = MIOPEN_REDUCE_TENSOR_MUL; constexpr miopenReduceTensorOp_t min = MIOPEN_REDUCE_TENSOR_MIN; constexpr miopenReduceTensorOp_t max = MIOPEN_REDUCE_TENSOR_MAX; constexpr miopenNanPropagation_t nan = MIOPEN_PROPAGATE_NAN; constexpr miopenNanPropagation_t nonan = MIOPEN_NOT_PROPAGATE_NAN; constexpr miopenReduceTensorIndices_t noind = MIOPEN_REDUCE_TENSOR_NO_INDICES; constexpr miopenReduceTensorIndices_t ind = MIOPEN_REDUCE_TENSOR_FLATTENED_INDICES; // clang-format off static std::vector test_cases = { std::make_tuple(std::vector{8, 2, 1}, std::vector{0, 1, 2}, add, nan, noind, alphabeta), std::make_tuple(std::vector{160, 10, 1}, std::vector{0, 1, 2}, add, nan, noind, alphabeta), std::make_tuple(std::vector{7, 1024, 1}, std::vector{0, 1, 2}, add, nan, noind, alphabeta), std::make_tuple(std::vector{3, 1, 1}, std::vector{0, 1, 2}, add, nan, noind, alphabeta), std::make_tuple(std::vector{3, 1, 1}, std::vector{0, 1, 2}, mul, nan, noind, alphabeta), std::make_tuple(std::vector{3, 1, 1}, std::vector{0, 1, 2}, max, nan, ind, alphabeta), std::make_tuple(std::vector{3, 2, 1}, std::vector{1, 2}, max, nan, ind, alphabeta), std::make_tuple(std::vector{6, 2, 1}, std::vector{1, 2}, max, nan, noind, alphabeta), std::make_tuple(std::vector{6, 2, 1}, std::vector{1, 2}, min, nan, noind, alphabeta), std::make_tuple(std::vector{2, 2, 1}, std::vector{1, 2}, add, nan, noind, alphabeta), std::make_tuple(std::vector{4, 3, 1}, std::vector{1, 2}, max, nan, noind, alphabeta), std::make_tuple(std::vector{3, 4, 1}, std::vector{1, 2}, max, nan, noind, alphabeta), std::make_tuple(std::vector{3, 4, 1}, std::vector{0, 2}, max, nan, noind, alphabeta), std::make_tuple(std::vector{2048, 32, 1}, std::vector{0, 2}, max, nan, noind, alphabeta), std::make_tuple(std::vector{4, 3, 1}, std::vector{1, 2}, min, nan, noind, alphabeta), std::make_tuple(std::vector{3, 4, 1}, std::vector{0, 2}, min, nan, noind, alphabeta), std::make_tuple(std::vector{2048, 32, 1}, std::vector{0, 2}, min, nan, noind, alphabeta), std::make_tuple(std::vector{12, 11, 1}, std::vector{0, 1, 2}, add, nan, noind, alphabeta), std::make_tuple(std::vector{13, 4, 7, 7},std::vector{0, 1, 2, 3}, add, nan, noind, alphabeta), std::make_tuple(std::vector{64, 3, 280, 81}, std::vector{0}, add, nonan, noind, alphabeta), }; // clang-format on return testing::ValuesIn(test_cases); } } // anonymous namespace template struct ReduceCommon : public testing::TestWithParam { void SetUp() override { auto&& handle = get_handle(); std::string device_name = handle.GetDeviceName(); prng::reset_seed(); handle.EnableProfiling(); std::tie(inLengths, toReduceDims, reduceOp, nanOpt, indicesOpt, scales) = GetParam(); } void Run() { using reduce::convert_type; if(std::is_same::value) compTypeVal = miopenDouble; if(std::is_same::value) { if(reduceOp == MIOPEN_REDUCE_TENSOR_MIN || reduceOp == MIOPEN_REDUCE_TENSOR_MAX || reduceOp == MIOPEN_REDUCE_TENSOR_AMAX) { compTypeVal = miopenHalf; // let compType be same as the data type } else { compTypeVal = miopenFloat; } } miopen::ReduceTensorDescriptor reduceDesc( reduceOp, compTypeVal, nanOpt, indicesOpt, indicesType); float alpha = scales[0]; float beta = scales[1]; // The test is ignored if (alpha, beta) is not (1.0f, 0.0f) and reduceOp is not Add/MUL/AVG if(reduceOp != MIOPEN_REDUCE_TENSOR_ADD && reduceOp != MIOPEN_REDUCE_TENSOR_MUL && reduceOp != MIOPEN_REDUCE_TENSOR_AVG && alpha != 1.0f && beta != 0.0f) { GTEST_SKIP(); } // The test is ignored if indices are requested but the reduceOp is neither MIN nor MAX if(indicesOpt != MIOPEN_REDUCE_TENSOR_NO_INDICES && reduceOp != MIOPEN_REDUCE_TENSOR_MIN && reduceOp != MIOPEN_REDUCE_TENSOR_MAX && reduceOp != MIOPEN_REDUCE_TENSOR_AMAX) { GTEST_SKIP(); } auto outLengths = this->inLengths; assert(toReduceDims.size() <= outLengths.size()); // set the lengths of the dimensions to be reduced to 1 to represent the output Tensor for(const int& toReduceDim : toReduceDims) { assert(toReduceDim < inLengths.size()); outLengths[toReduceDim] = static_cast(1); } uint64_t max_value; if(reduceOp == MIOPEN_REDUCE_TENSOR_MUL) { max_value = miopen_type{} == miopenHalf ? 41 : miopen_type{} == miopenInt8 ? 127 : 111; } else if(reduceOp == MIOPEN_REDUCE_TENSOR_NORM1 || reduceOp == MIOPEN_REDUCE_TENSOR_NORM2) { max_value = 3; } else { max_value = miopen_type{} == miopenHalf ? 13 : miopen_type{} == miopenInt8 ? 127 : 999; } // default data gneration (used by MIN/MAX) auto gen_value_min_max = [&](auto... is) { return (tensor_elem_gen_integer{max_value}(is...) * tensor_elem_gen_checkboard_sign{}(is...)); }; // data generation used by ADD/AVG, data is distributed around 1.0 rather than 0.0, very low // probability to get a reduced result of zero-value auto gen_value_add_avg = [&](auto... is) { auto rand_value = tensor_elem_gen_integer{max_value}(is...); auto sign_value = tensor_elem_gen_checkboard_sign{}(is...); return (sign_value * rand_value / max_value + 0.01); }; // Special data generation for MUL, to avoid all-zero and large accumulative error in the // reduced result auto gen_value_mul = [&](auto... is) { auto rand_value = tensor_elem_gen_integer{max_value}(is...); auto sign_value = tensor_elem_gen_checkboard_sign{}(is...); return sign_value > 0.0 ? (rand_value + max_value) / (rand_value + max_value + 1) : (rand_value + max_value + 1) / (rand_value + max_value); }; // Special data generation for NORM1 and NORM2 using a space of limitless number of values. auto gen_value_norm1_norm2 = [&](auto... is) { auto rand_upper = tensor_elem_gen_integer{max_value}(is...); auto sign_value = tensor_elem_gen_checkboard_sign{}(is...); auto rand_ratio = prng::gen_A_to_B( 0.1, 1.); // limit range due to numeric errors, see WORKAROUND_GPU_NUMERIC_ERROR return rand_upper * sign_value * rand_ratio; }; // Special data generation for AMAX, no zero value used auto gen_value_amax = [&](auto... is) { auto rand_value = tensor_elem_gen_integer{max_value}(is...); auto sign_value = tensor_elem_gen_checkboard_sign{}(is...); return sign_value > 0.0 ? (rand_value + 0.5) : (-1.0 * rand_value - 0.5); }; // default tolerance (refer to driver.hpp) this->tolerance = 80; if(reduceOp == MIOPEN_REDUCE_TENSOR_ADD || reduceOp == MIOPEN_REDUCE_TENSOR_AVG) this->tolerance = 80 * 10; if(reduceOp == MIOPEN_REDUCE_TENSOR_MUL) { this->tolerance = 80 * 300; } else if(reduceOp == MIOPEN_REDUCE_TENSOR_NORM1 || reduceOp == MIOPEN_REDUCE_TENSOR_NORM2) { if(toReduceDims.size() == 4) this->tolerance = 80 * 100; else this->tolerance = 80 * 10; }; if(std::is_same::value) this->tolerance *= this->tolerance * 10.0; tensor inputTensor; switch(reduceOp) { case MIOPEN_REDUCE_TENSOR_ADD: case MIOPEN_REDUCE_TENSOR_AVG: inputTensor = tensor{this->inLengths}.generate(gen_value_add_avg); break; case MIOPEN_REDUCE_TENSOR_MUL: inputTensor = tensor{this->inLengths}.generate(gen_value_mul); break; case MIOPEN_REDUCE_TENSOR_NORM1: case MIOPEN_REDUCE_TENSOR_NORM2: inputTensor = tensor{this->inLengths}.generate(gen_value_norm1_norm2); break; case MIOPEN_REDUCE_TENSOR_AMAX: inputTensor = tensor{this->inLengths}.generate(gen_value_amax); break; case MIOPEN_REDUCE_TENSOR_MIN: case MIOPEN_REDUCE_TENSOR_MAX: inputTensor = tensor{this->inLengths}.generate(gen_value_min_max); }; auto outputTensor = tensor{outLengths}; std::fill(outputTensor.begin(), outputTensor.end(), convert_type(0.0f)); auto indices_nelem = reduceDesc.GetIndicesSize(inputTensor.desc, outputTensor.desc) / sizeof(int); auto ws_sizeInBytes = reduceDesc.GetWorkspaceSize(get_handle(), inputTensor.desc, outputTensor.desc); auto workspace_nelem = (indices_nelem == 0) ? ws_sizeInBytes / sizeof(T) : (ws_sizeInBytes + sizeof(T) - 1) / sizeof(T); std::vector wsLengths = {static_cast(workspace_nelem), 1}; auto workspaceTensor = tensor{wsLengths}; std::fill(workspaceTensor.begin(), workspaceTensor.end(), convert_type(0.0f)); if(indices_nelem > 0) { std::vector indicesLengths = {static_cast(indices_nelem), 1}; auto indicesTensor = tensor{indicesLengths}; std::fill(indicesTensor.begin(), indicesTensor.end(), 1); VerifyReduceWithIndices( reduceDesc, inputTensor, outputTensor, workspaceTensor, indicesTensor, 1.0f, 0.0f); VerifyReduceWithIndices( reduceDesc, inputTensor, outputTensor, workspaceTensor, indicesTensor, 1.0f, 0.0f); } else { VerifyReduceNoIndices( reduceDesc, inputTensor, outputTensor, workspaceTensor, alpha, beta); }; }; private: template void VerifyReduceWithIndices(const miopen::ReduceTensorDescriptor& reduce, const tensor& input, const tensor& output, const tensor& workspace, const tensor& indices, float alpha, float beta) const { verify_reduce_with_indices reduce_with_indices( reduce, input, output, workspace, indices, alpha, beta); CompareResults(reduce_with_indices, toVerifyData); } void VerifyReduceNoIndices(const miopen::ReduceTensorDescriptor& reduce, const tensor& input, const tensor& output, const tensor& workspace, float alpha, float beta) const { verify_reduce_no_indices reduce_no_indices( reduce, input, output, workspace, alpha, beta); CompareResults(reduce_no_indices, true); } std::string GetOutputValuesForError() const { std::ostringstream oss; oss << "reduceOp: " << reduceOp << std::endl << "compTypeVal: " << compTypeVal << std::endl << "nanOpt: " << nanOpt << std::endl << "indicesOpt: " << indicesOpt << std::endl; return oss.str(); } template void CompareResults(const TVerifier& verifier, bool toVerifyData) const { const tensor cpu = std::move(verifier.cpu()); const tensor gpu = std::move(verifier.gpu()); if(toVerifyData) { double threshold = std::numeric_limits::epsilon() * tolerance; double error = miopen::rms_range(cpu, gpu); if(error > threshold) { verifier.fail(); std::cout << "Tolerance: " << tolerance << std::endl; } EXPECT_LE(error, threshold) << GetOutputValuesForError(); } else { auto idx = miopen::mismatch_idx(cpu, gpu, miopen::float_equal); auto range = miopen::range_distance(cpu); if(idx < range) { verifier.fail(); } EXPECT_GE(idx, range) << GetOutputValuesForError(); } } private: miopenReduceTensorOp_t reduceOp = MIOPEN_REDUCE_TENSOR_ADD; miopenDataType_t compTypeVal = miopenFloat; miopenNanPropagation_t nanOpt = MIOPEN_NOT_PROPAGATE_NAN; miopenReduceTensorIndices_t indicesOpt = MIOPEN_REDUCE_TENSOR_NO_INDICES; miopenIndicesType_t indicesType = MIOPEN_32BIT_INDICES; std::vector inLengths; // the lengths of the input tensor's dimensions std::vector toReduceDims; // the indexes of the dimensions to be reduced in the input tensor std::vector scales; double tolerance = 0.0; // will be calculated during test execution }; using GPU_Reduce_FP32 = ReduceCommon; using GPU_Reduce_FP16 = ReduceCommon; using GPU_Reduce_FP64 = ReduceCommon; TEST_P(GPU_Reduce_FP32, TestFloat) { this->Run(); } TEST_P(GPU_Reduce_FP16, TestFloat16) { this->Run(); } TEST_P(GPU_Reduce_FP64, TestDouble) { this->Run(); } // Tiered instantiation: Smoke (pre-commit) and Standard (per-CI) run small // subsets; Full (comprehensive/nightly) runs the complete cross product so no // coverage is lost. See GenCasesSmoke/GenCasesStandard above. INSTANTIATE_TEST_SUITE_P(Smoke, GPU_Reduce_FP32, GetCasesSmoke()); INSTANTIATE_TEST_SUITE_P(Standard, GPU_Reduce_FP32, GetCasesStandard()); INSTANTIATE_TEST_SUITE_P(Full, GPU_Reduce_FP32, GetCasesFull()); INSTANTIATE_TEST_SUITE_P(Smoke, GPU_Reduce_FP16, GetCasesSmoke()); INSTANTIATE_TEST_SUITE_P(Standard, GPU_Reduce_FP16, GetCasesStandard()); INSTANTIATE_TEST_SUITE_P(Full, GPU_Reduce_FP16, GetCasesFull()); INSTANTIATE_TEST_SUITE_P(Full, GPU_Reduce_FP64, GetCasesFull()); // Reduce Custom Tests template class ReduceCustomCommon : public ReduceCommon { void SetUp() override { using e_mask = enabled; using d_mask = disabled; if(!::IsTestSupportedForDevMask()) { GTEST_SKIP(); } ReduceCommon::SetUp(); } }; // original file reduce_custom_fp32.cpp class GPU_reduce_custom_fp32_FP32 : public ReduceCustomCommon { }; class GPU_reduce_custom_fp32_FP16 : public ReduceCustomCommon { }; class GPU_reduce_custom_fp32_BFP16 : public ReduceCustomCommon { }; class GPU_reduce_custom_fp32_I8 : public ReduceCustomCommon { }; TEST_P(GPU_reduce_custom_fp32_FP32, FloatTest_reduce_custom_fp32) { this->Run(); }; INSTANTIATE_TEST_SUITE_P(Full, GPU_reduce_custom_fp32_FP32, GetCasesReduceCustomTestSet1()); TEST_P(GPU_reduce_custom_fp32_FP16, CAN_BE_DISABLED_HalfTest_reduce_custom_fp16) { this->Run(); }; INSTANTIATE_TEST_SUITE_P(Full, GPU_reduce_custom_fp32_FP16, GetCasesReduceCustomTestSet1()); TEST_P(GPU_reduce_custom_fp32_BFP16, CAN_BE_DISABLED_BHalfTest_reduce_custom_fp16) { this->Run(); }; INSTANTIATE_TEST_SUITE_P(Full, GPU_reduce_custom_fp32_BFP16, GetCasesReduceCustomTestSet1()); TEST_P(GPU_reduce_custom_fp32_I8, CAN_BE_DISABLED_IntTest_reduce_custom_fp16) { this->Run(); }; INSTANTIATE_TEST_SUITE_P(Full, GPU_reduce_custom_fp32_I8, GetCasesReduceCustomTestSet1()); // original file reduce_custom_fp32_fp16.cpp class GPU_reduce_custom_fp32_fp16_FP32 : public ReduceCustomCommon { }; class GPU_reduce_custom_fp32_fp16_FP16 : public ReduceCustomCommon { }; TEST_P(GPU_reduce_custom_fp32_fp16_FP32, FloatTest_reduce_custom_fp32_fp16) { this->Run(); }; TEST_P(GPU_reduce_custom_fp32_fp16_FP16, HalfTest_reduce_custom_fp32_fp16) { this->Run(); }; INSTANTIATE_TEST_SUITE_P(Full, GPU_reduce_custom_fp32_fp16_FP32, GetCasesReduceCustomTestSet2()); INSTANTIATE_TEST_SUITE_P(Full, GPU_reduce_custom_fp32_fp16_FP16, GetCasesReduceCustomTestSet2());