/******************************************************************************* * * MIT License * * Copyright (c) 2023 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. * *******************************************************************************/ #pragma once #include "../random.hpp" #include "get_handle.hpp" #include #include #include #include "../driver/tensor_driver.hpp" #include "conv_common.hpp" #include "gtest_common.hpp" namespace group_conv { using Direction = miopen::conv::Direction; template struct GroupConvTestConfig { }; template <> struct GroupConvTestConfig<2u> { struct Size2D { size_t y; size_t x; }; size_t G; size_t N; size_t C; size_t K; Size2D img; Size2D filter; Size2D pad; Size2D stride; Size2D dilation; friend std::ostream& operator<<(std::ostream& os, const GroupConvTestConfig& tc) { return os << " G:" << tc.G << " N:" << tc.N << " C:" << tc.C << " K:" << tc.K << " H:" << tc.img.y << " W:" << tc.img.x << " y:" << tc.filter.y << " x:" << tc.filter.x << " pad.y:" << tc.pad.y << " pad.x:" << tc.pad.x << " stride.y:" << tc.stride.y << " stride.x" << tc.stride.x << " dilation.y:" << tc.dilation.y << " dilation.x" << tc.dilation.x; } std::vector GetInput() { return {N, C, img.y, img.x}; } std::vector GetWeights() { EXPECT_EQUAL(C % G, 0); return {K, C / G, filter.y, filter.x}; } miopen::ConvolutionDescriptor GetConv() { return miopen::ConvolutionDescriptor{ 2, miopenConvolution, miopenPaddingDefault, {static_cast(pad.y), static_cast(pad.x)}, {static_cast(stride.y), static_cast(stride.x)}, {static_cast(dilation.y), static_cast(dilation.x)}, {0, 0}, static_cast(G), 1.0}; } template static std::vector GetConfigs() { if constexpr(DIR == Direction::Forward) { // clang-format off return { // g n C K img filter pad stride dilation {1 , 256, 192 , 192 , {28, 28} , {3, 3}, {1, 1}, {1, 1}, {1, 1}}, {1 , 256, 12 , 12 , {28, 28} , {3, 3}, {1, 1}, {1, 1}, {1, 1}}, {4 , 256, 192 , 192 , {28, 28} , {3, 3}, {1, 1}, {1, 1}, {1, 1}}, {8 , 256, 192 , 192 , {28, 28} , {3, 3}, {1, 1}, {1, 1}, {1, 1}}, {8 , 256, 384 , 384 , {28, 28} , {3, 3}, {1, 1}, {1, 1}, {1, 1}}, {32, 256, 1024, 2048, {28, 28} , {3, 3}, {1, 1}, {1, 1}, {1, 1}}, {4 , 256, 192 , 192 , {28, 28} , {3, 3}, {1, 1}, {2, 2}, {1, 1}}, {8 , 256, 384 , 384 , {28, 28} , {3, 3}, {1, 1}, {2, 2}, {1, 1}}, {32, 256, 1024, 2048, {28, 28} , {3, 3}, {1, 1}, {2, 2}, {1, 1}}, {1 , 6 , 448 , 896 , {118, 182}, {3, 3}, {0, 0}, {2, 2}, {1, 1}}, {4 , 256, 192 , 192 , {28, 28} , {1, 1}, {1, 1}, {2, 2}, {1, 1}}, {8 , 256, 384 , 384 , {28, 28} , {1, 1}, {1, 1}, {2, 2}, {1, 1}}, {32, 256, 1024, 2048, {28, 28} , {1, 1}, {1, 1}, {2, 2}, {1, 1}}, {1 , 6 , 448 , 896 , {118, 182}, {1, 1}, {0, 0}, {2, 2}, {1, 1}}, {4 , 16 , 224 , 224 , {469, 724}, {3, 3}, {1, 1}, {2, 2}, {1, 1}}, }; // clang-format on } else if constexpr(DIR == Direction::BackwardData || DIR == Direction::BackwardWeights) { // clang-format off return { // g n C K img filter pad stride dilation {1 , 1 , 1 , 1 , {28, 28} , {3, 3}, {1, 1}, {1, 1}, {1, 1}}, {1 , 1 , 4 , 4 , {28, 28} , {2, 2}, {1, 1}, {1, 1}, {1, 1}}, {1 , 1 , 1 , 1 , {8 , 8 } , {2, 2}, {0, 0}, {1, 1}, {1, 1}}, {8 , 256, 192 , 192 , {28, 28} , {3, 3}, {1, 1}, {1, 1}, {1, 1}}, {8 , 256, 384 , 384 , {28, 28} , {2, 2}, {1, 1}, {1, 1}, {1, 1}}, {32, 256, 1024, 2048, {28, 28} , {3, 3}, {1, 1}, {1, 1}, {1, 1}}, {1 , 6 , 448 , 896 , {118, 182}, {3, 3}, {0, 0}, {1, 1}, {1, 1}}, {8 , 256, 384 , 384 , {28, 28} , {2, 2}, {1, 1}, {2, 2}, {1, 1}}, {32, 256, 1024, 2048, {28, 28} , {3, 3}, {1, 1}, {2, 2}, {1, 1}}, {1 , 6 , 448 , 896 , {118, 182}, {1, 1}, {0, 0}, {2, 2}, {1, 1}}, // Todo: test failed for fp32 on mi200 for wrw. Need more analysis //{4 , 16 , 224 , 224 , {469, 724}, {3, 3}, {1, 1}, {2, 2}, {1, 1}}, }; // clang-format on } else { std::abort(); } } }; template <> struct GroupConvTestConfig<3u> { struct Size3D { size_t z; size_t y; size_t x; }; size_t G; size_t N; size_t C; size_t K; Size3D img; Size3D filter; Size3D pad; Size3D stride; Size3D dilation; friend std::ostream& operator<<(std::ostream& os, const GroupConvTestConfig<3u>& tc) { return os << " G:" << tc.G << " N:" << tc.N << " C:" << tc.C << " K:" << tc.K << " D:" << tc.img.z << " H:" << tc.img.y << " W:" << tc.img.x << " z:" << tc.filter.z << " y:" << tc.filter.y << " x:" << tc.filter.x << " pad.z:" << tc.pad.z << " pad.y:" << tc.pad.y << " pad.x:" << tc.pad.x << " stride.z:" << tc.stride.z << " stride.y:" << tc.stride.y << " stride.x:" << tc.stride.x << " dilation.z:" << tc.dilation.z << " dilation.y:" << tc.dilation.y << " dilation.x:" << tc.dilation.x; } std::vector GetInput() { return {N, C, img.z, img.y, img.x}; } std::vector GetWeights() { EXPECT_EQUAL(C % G, 0); return {K, C / G, filter.z, filter.y, filter.x}; } miopen::ConvolutionDescriptor GetConv() { return miopen::ConvolutionDescriptor{ 3, miopenConvolution, miopenPaddingDefault, {static_cast(pad.z), static_cast(pad.y), static_cast(pad.x)}, {static_cast(stride.z), static_cast(stride.y), static_cast(stride.x)}, {static_cast(dilation.z), static_cast(dilation.y), static_cast(dilation.x)}, {0, 0, 0}, static_cast(G), 1.0}; } template static std::vector GetConfigs() { if constexpr(DIR == Direction::Forward) { // clang-format off return { // g n C K img filter pad stride dilation {1 , 128, 64 , 64 , {14, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {1 , 64 , 32 , 32 , {28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {2 , 128, 32 , 32 , {28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {8 , 128, 32 , 32 , {28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {2 , 128, 32 , 32 , {28, 28, 28} , {3, 3, 3}, {0, 0, 0}, {1, 1, 1}, {1, 1, 1}}, {8 , 64 , 32 , 32 , {28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {16, 64 , 32 , 32 , {28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {2 , 128, 32 , 32 , {28, 28, 28} , {3, 3, 3}, {0, 0, 0}, {2, 2, 2}, {1, 1, 1}}, {8 , 64 , 32 , 32 , {28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {2, 2, 2}, {1, 1, 1}}, {16, 64 , 32 , 32 , {28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {2, 2, 2}, {1, 1, 1}}, {3 , 48 , 48 , 48 , {28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {3 , 48 , 39 , 39 , {28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {5 , 120, 60 , 60 , {28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {1 , 6 , 448, 896, {3, 118, 182}, {1, 1, 1}, {0, 0, 0}, {1, 2, 2}, {1, 1, 1}}, {1 , 256, 3 ,1280, {2, 14, 14} , {2,14,14}, {0, 0, 0}, {2,14,14}, {1, 1, 1}}, {5 , 256, 60 ,1280, {2, 14, 14} , {2,14,14}, {0, 0, 0}, {2,14,14}, {1, 1, 1}}, {3 , 256, 9, 27, {2, 14, 14} , {2,14,14}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {9 , 256, 9, 27, {2, 14, 14} , {2,14,14}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, }; // clang-format on } else if constexpr(DIR == Direction::BackwardData || DIR == Direction::BackwardWeights) { // clang-format off return { // g n C K img filter pad stride dilation {1, 1 , 4 , 4 ,{14, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {1, 1 , 1 , 1 ,{4 , 4 , 4 } , {2, 2, 2}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {1, 1 , 1 , 1 ,{8 , 8 , 8 } , {2, 2, 2}, {0, 0, 0}, {1, 1, 1}, {1, 1, 1}}, {1, 1 , 1 , 1 ,{8 , 8 , 8 } , {2, 2, 2}, {0, 0, 0}, {2, 2, 2}, {1, 1, 1}}, {1, 64 , 32, 16 ,{28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {8, 128, 16, 32 ,{28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {8, 128, 16, 16 ,{28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {4, 128, 8 , 4 ,{28, 28, 28} , {2, 2, 2}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {4, 128, 4 , 8 ,{28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {2, 128, 2 , 2 ,{28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {8, 128, 16, 16 ,{28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {2, 2, 2}, {1, 1, 1}}, {4, 128, 8 , 4 ,{28, 28, 28} , {2, 2, 2}, {1, 1, 1}, {2, 2, 2}, {1, 1, 1}}, {4, 128, 4 , 8 ,{28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {2, 2, 2}, {1, 1, 1}}, {2, 128, 2 , 2 ,{28, 28, 28} , {3, 3, 3}, {1, 1, 1}, {2, 2, 2}, {1, 1, 1}}, {1, 6 , 448, 896,{3, 118, 182}, {1, 1, 1}, {0, 0, 0}, {1, 2, 2}, {1, 1, 1}}, {1, 256, 3 ,1280,{2, 14, 14} , {2,14,14}, {0, 0, 0}, {2,14,14}, {1, 1, 1}}, {5, 256, 60 ,1280,{2, 14, 14} , {2,14,14}, {0, 0, 0}, {2,14,14}, {1, 1, 1}}, {3, 256, 9, 27,{2, 14, 14} , {2,14,14}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, {9, 256, 9, 27,{2, 14, 14} , {2,14,14}, {1, 1, 1}, {1, 1, 1}, {1, 1, 1}}, }; // clang-format on } else { std::abort(); } } }; template struct GroupConvTestFix : public ::testing::TestWithParam< std::tuple, float, float, miopenTensorLayout_t>> { static_assert(NDIM == 2u || NDIM == 3u, "NDIM must be 2 for 2D Conv and 3 for 3D Conv"); private: using Base = ::testing::TestWithParam< std::tuple, float, float, miopenTensorLayout_t>>; template void SetupFwd(F&& gen_value) { input.generate(gen_value); weights.generate(gen_value); std::fill(output.begin(), output.end(), T(0)); } template void SetupBwd(F&& gen_value) { output.generate(gen_value); weights.generate(gen_value); std::fill(input.begin(), input.end(), T(0)); } template void SetupWrw(FI&& gen_value_in, FO&& gen_value_out) { input.generate(gen_value_in); output.generate(gen_value_out); std::fill(weights.begin(), weights.end(), T{0}); } void verify(const tensor& computed) { EXPECT_FALSE(miopen::range_zero(ref)) << "Cpu data is all zeros"; EXPECT_FALSE(miopen::range_zero(computed)) << "Gpu data is all zeros"; EXPECT_TRUE(miopen::range_distance(ref) == miopen::range_distance(computed)); /// \todo figure out a better threshold for error checking, esp. for bwd /// data and weight passes. --amberhassaan double threshold = 80; if(CONV_DIR == Direction::Forward) { if constexpr(std::is_same_v) { // float use tf32 compute which share same mantissa bits threshold *= (compute_type == "TF32") ? std::numeric_limits::epsilon() : std::numeric_limits::epsilon(); } else { threshold *= std::numeric_limits::epsilon(); } } else { // some kernels have an error above 0.3%, so this has been increased to 0.4% threshold = 4.0e-3; } auto error = miopen::rms_range(ref, computed); EXPECT_FALSE(miopen::find_idx(ref, miopen::not_finite) >= 0) << "Non finite number found in the reference output"; EXPECT_TRUE(error <= threshold) << "Error beyond tolerance Error:" << error << ", Threshold: " << threshold; } /// \todo had to pull out tensor and problem construction because the order of /// tensors and tensor-descriptors varies by direction. Will move these /// constructors back in this method once we have a uniform order of (x, w, y) /// tensors everywhere. --amberhassaan template void RunSolverImpl(const ConvTensorsType& tensors, const ProblemDescription& problem) { std::cout << conv_config << std::endl; auto&& handle = get_handle(); Solver solv{}; auto ctx = miopen::ExecutionContext{}; ctx.SetStream(&handle); if(compute_type == "TF32") problem.SetupComputeType(ctx); if(!solv.IsApplicable(ctx, problem)) { test_skipped = true; GTEST_SKIP() << solv.SolverDbId() << "Not Applicable for this problem" << conv_config; } if(solv.MayNeedWorkspace()) { wspace.resize(solv.GetWorkspaceSize(ctx, problem)); } const auto invoke_params = InvokeParamType{tensors, wspace.ptr(), wspace.size(), false, alpha, beta}; ASSERT_TRUE(solv.IsApplicable(ctx, problem)); auto sol = solv.GetSolution(ctx, problem, solv.GetDefaultPerformanceConfig(ctx, problem)); ASSERT_TRUE(sol.Succeeded()); ASSERT_TRUE(sol.invoker_factory); const auto invoker = handle.PrepareInvoker(*sol.invoker_factory, sol.construction_params); (invoker)(handle, invoke_params); handle.Finish(); } template void DispatchSolver() { if constexpr(CONV_DIR == Direction::Forward) { RunSolverImpl( miopen::ConvDataTensors{input.desc, in_dev.get(), weights.desc, wei_dev.get(), output.desc, out_dev.get()}, miopen::conv::ProblemDescription{input.desc, weights.desc, output.desc, conv_desc, CONV_DIR, 0 /*bias*/, alpha, beta}); } else if constexpr(CONV_DIR == Direction::BackwardData) { RunSolverImpl( miopen::ConvDataTensors{output.desc, out_dev.get(), weights.desc, wei_dev.get(), input.desc, in_dev.get()}, miopen::conv::ProblemDescription{output.desc, weights.desc, input.desc, conv_desc, CONV_DIR, 0 /*bias*/, alpha, beta}); } else { static_assert(CONV_DIR == Direction::BackwardWeights); RunSolverImpl( miopen::ConvWrwTensors{output.desc, out_dev.get(), input.desc, in_dev.get(), weights.desc, wei_dev.get()}, miopen::conv::ProblemDescription{output.desc, weights.desc, input.desc, conv_desc, CONV_DIR, 0 /*bias*/, alpha, beta}); } } public: void RunSolver() { if constexpr(NDIM == 2u) { DispatchSolver(); } else { DispatchSolver(); } } protected: void SetUp() override { if constexpr(CONV_DIR == Direction::BackwardWeights) { if(!IsTestSupportedByDevice(Gpu::gfx94X) && std::is_same::value) { test_skipped = true; GTEST_SKIP() << "bf16 tests skipped on this hardware."; } if(!IsTestSupportedByDevice(Gpu::gfx94X) && compute_type == "TF32") { test_skipped = true; GTEST_SKIP() << "tf32 tests skipped on this hardware."; } } float alpha_val; float beta_val; test_skipped = false; std::tie(conv_config, alpha_val, beta_val, tensor_layout) = Base::GetParam(); alpha = miopen::Scalar(&alpha_val, miopenFloat); beta = miopen::Scalar(&beta_val, miopenFloat); input = tensor{tensor_layout, conv_config.GetInput()}; weights = tensor{tensor_layout, conv_config.GetWeights()}; conv_desc = conv_config.GetConv(); if(compute_type == "TF32") conv_desc.attribute.Set(MIOPEN_CONVOLUTION_ATTRIB_MATH_TYPE, miopenMathDefault); else conv_desc.attribute.Set(MIOPEN_CONVOLUTION_ATTRIB_MATH_TYPE, miopenMathPedantic); miopen::TensorDescriptor output_desc = conv_desc.GetForwardOutputTensor(input.desc, weights.desc, miopen_type{}); output = tensor{tensor_layout, output_desc.GetLengths()}; auto gen_value = [](auto...) { return prng::gen_A_to_B(static_cast(-3.0), static_cast(3.0)); }; if constexpr(CONV_DIR == Direction::Forward) { SetupFwd(gen_value); } else if constexpr(CONV_DIR == Direction::BackwardData) { SetupBwd(gen_value); } else { // Half16 can store up to 16384. // If we initialize tensor with 5 * number_of_accumulations in tensor // this will cause over flow. Hence we pick smaller number, since // our tests have tensor with large sizes. auto gen_value_wrw_in = [](auto...) { return prng::gen_A_to_B(static_cast(-0.1), static_cast(0.1)); }; auto gen_value_wrw_out = [](auto...) { return prng::gen_A_to_B(static_cast(-0.01), static_cast(0.1)); }; static_assert(CONV_DIR == Direction::BackwardWeights); // in and out are populated with different values. SetupWrw(gen_value_wrw_in, gen_value_wrw_out); } auto& handle = get_handle(); in_dev = handle.Write(input.data); wei_dev = handle.Write(weights.data); out_dev = handle.Write(output.data); } void TearDown() override { if(test_skipped) return; auto& handle = get_handle(); if constexpr(CONV_DIR == Direction::Forward) { ref = ref_conv_fwd(input, weights, output, conv_desc, alpha, beta); handle.ReadToVec(out_dev, output.data); verify(output); } else if constexpr(CONV_DIR == Direction::BackwardData) { ref = ref_conv_bwd(input, weights, output, conv_desc, alpha, beta); handle.ReadToVec(in_dev, input.data); verify(input); } else { static_assert(CONV_DIR == Direction::BackwardWeights); ref = ref_conv_wrw(input, weights, output, conv_desc, alpha, beta); handle.ReadToVec(wei_dev, weights.data); verify(weights); } } GroupConvTestConfig conv_config; miopen::ConvolutionDescriptor conv_desc; tensor input; tensor weights; tensor output; tensor ref; miopen::Allocator::ManageDataPtr in_dev; miopen::Allocator::ManageDataPtr wei_dev; miopen::Allocator::ManageDataPtr out_dev; bool test_skipped = false; miopenTensorLayout_t tensor_layout = miopenTensorNHWC; Workspace wspace{}; miopen::Scalar alpha{1.0}; miopen::Scalar beta{0.0}; std::string compute_type; }; template std::vector GetLayoutValues() { static_assert(NDIM == 2u || NDIM == 3u); if constexpr(NDIM == 2u) { return {miopenTensorNHWC, miopenTensorNCHW}; } else { return {miopenTensorNDHWC, miopenTensorNCDHW}; } } } // namespace group_conv // Test case based on 2d vs 3d dimension // 2d conv only support alpha 1.0 template std::vector GetAlphaValues() { if constexpr(ND == 3) { return {1.0f, 2.2f}; /* alpha, can't be zero*/ } else { return {1.0f}; } } // Test case based on 2d vs 3d dimension // 2d conv only support beta 0.0 template std::vector GetBetaValues() { if constexpr(ND == 3) { return {0.0f, 3.3f}; } else { return {0.0f}; } } #define DEFINE_GROUP_CONV_TEST(ndim, type, naming_type, dir) \ struct GPU_GroupConv##ndim##D_##dir##_##naming_type \ : GroupConvTestFix \ { \ GPU_GroupConv##ndim##D_##dir##_##naming_type() { compute_type = #naming_type; } \ }; \ TEST_P(GPU_GroupConv##ndim##D_##dir##_##naming_type, GroupConv##ndim##D_##dir##_##type##_Test) \ { \ RunSolver(); \ } \ INSTANTIATE_TEST_SUITE_P( \ Full, \ GPU_GroupConv##ndim##D_##dir##_##naming_type, \ testing::Combine( \ testing::ValuesIn(GroupConvTestConfig::GetConfigs()), \ testing::ValuesIn(GetAlphaValues()), \ testing::ValuesIn(GetBetaValues()), \ testing::ValuesIn(GetLayoutValues()))); #define DEFINE_GROUP_CONV2D_TEST(type, naming_type, dir) \ DEFINE_GROUP_CONV_TEST(2, type, naming_type, dir) #define DEFINE_GROUP_CONV3D_TEST(type, naming_type, dir) \ DEFINE_GROUP_CONV_TEST(3, type, naming_type, dir)