diff --git a/modules/dnn/src/dnn.cpp b/modules/dnn/src/dnn.cpp index f650a71fc2..8947791061 100644 --- a/modules/dnn/src/dnn.cpp +++ b/modules/dnn/src/dnn.cpp @@ -2458,7 +2458,7 @@ struct Net::Impl : public detail::NetImplBase if( nextData ) nextActivLayer = nextData->layerInstance.dynamicCast(); - if( !nextActivLayer.empty() && pinsToKeep.count(lpNext) == 0 && + if( !nextActivLayer.empty() && (!nextData->type.compare("ReLU") || !nextData->type.compare("ChannelsPReLU") || !nextData->type.compare("Power")) && diff --git a/modules/dnn/test/test_layers.cpp b/modules/dnn/test/test_layers.cpp index 0c4ce11ca5..648e0aaa16 100644 --- a/modules/dnn/test/test_layers.cpp +++ b/modules/dnn/test/test_layers.cpp @@ -2053,4 +2053,436 @@ TEST_P(Layer_Test_BatchNorm, fusion) INSTANTIATE_TEST_CASE_P(/**/, Layer_Test_BatchNorm, dnnBackendsAndTargets()); +class TestLayerFusion : public DNNTestLayer { +public: + static void makeDefaultTestConvolutionLayer(LayerParams& convParams, int in_channels, int num_filters, bool bias_term) + { + const int kernel_h = 3, kernel_w = 3; + const int pad_h = kernel_h / 2, pad_w = kernel_w / 2; + + convParams.set("kernel_h", kernel_h); + convParams.set("kernel_w", kernel_w); + convParams.set("pad_h", pad_h); + convParams.set("pad_w", pad_w); + convParams.set("num_output", num_filters); + convParams.set("bias_term", bias_term); + convParams.type = "Convolution"; + convParams.name = "convolution"; + + float conv_init_magnitude = 1.0f / in_channels / kernel_h / kernel_w; + int weightsShape[] = {num_filters, in_channels, kernel_h, kernel_w}; + Mat weights(4, &weightsShape[0], CV_32F); + randu(weights, -conv_init_magnitude, conv_init_magnitude); + convParams.blobs.push_back(weights); + if (bias_term) + { + Mat bias(1, num_filters, CV_32F); + randu(bias, -1.0f, 1.0f); + convParams.blobs.push_back(bias); + } + } + + static void makeDefaultTestActivationLayer(LayerParams& activationParams, const std::string& type, int in_channels) + { + activationParams.type = type; + activationParams.name = "activation"; + if (activationParams.type == "ReLU") + activationParams.set("negative_slope", 0.1f); + else if (activationParams.type == "Power") + { + activationParams.set("power", 2.0f); + activationParams.set("scale", 0.5f); + activationParams.set("shift", 0.3f); + } + else if (activationParams.type == "ReLU6") + { + activationParams.set("min_value", -1.0f); + activationParams.set("max_value", 1.0f); + } + else if (activationParams.type == "ChannelsPReLU") + { + Mat scales(1, in_channels, CV_32F); + randu(scales, -1.0f, 1.0f); + activationParams.blobs.push_back(scales); + } + } + + static void makeDefaultTestEltwiseLayer(LayerParams& eltwiseParams, const std::string& op, bool withCoefficients) + { + eltwiseParams.type = "Eltwise"; + eltwiseParams.name = "eltwise"; + eltwiseParams.set("operation", op); + if (withCoefficients) + { + float coeff[] = {0.3f, 0.5f}; + eltwiseParams.set("coeff", DictValue::arrayReal(coeff, 2)); + } + } + + static void test(Mat& input, Net& net, Backend backendId, Target targetId, std::vector expectedFusedLayers = std::vector(), double l1 = 0.0, double lInf = 0.0) + { + DNNTestLayer::checkBackend(backendId, targetId); + + net.enableFusion(false); + net.setPreferableBackend(DNN_BACKEND_OPENCV); + net.setPreferableTarget(DNN_TARGET_CPU); + net.setInput(input); + Mat outputReference = net.forward().clone(); + std::vector refTimings; + net.getPerfProfile(refTimings); + for (int i = 0; i < refTimings.size(); i++) + { + CV_Assert(refTimings[i] != 0.0); + } + + net.enableFusion(true); + net.setPreferableBackend(backendId); + net.setPreferableTarget(targetId); + net.setInput(input); + Mat outputTest = net.forward().clone(); + std::vector testTimings; + net.getPerfProfile(testTimings); + for (int i = 0; i < testTimings.size(); i++) + { + if(std::find(expectedFusedLayers.begin(), expectedFusedLayers.end(), i + 1) != expectedFusedLayers.end()) + { + EXPECT_EQ(testTimings[i], 0.0); + } + else + { + EXPECT_NE(testTimings[i], 0.0); + } + } + + // double ref_max_value, ref_min_value; + // minMaxLoc(outputReference.reshape(1, 1), &ref_min_value, &ref_max_value); + // std::cout << "reference range: " << ref_min_value << ' ' << ref_max_value << std::endl; + + double default_l1, default_lInf; + DNNTestLayer::getDefaultThresholds(backendId, targetId, &default_l1, &default_lInf); + if (l1 == 0.0) + l1 = default_l1; + if (lInf == 0.0) + lInf = default_lInf; + normAssert(outputReference, outputTest, "", l1, lInf); + } + + static testing::internal::ParamGenerator eltwiseOpList() + { + // TODO: automate list generation + return Values("sum", "max", "prod", "div"); + } + + static testing::internal::ParamGenerator activationLayersList() + { + // TODO: automate list generation + return Values("ReLU", "ReLU6", "ChannelsPReLU", "TanH", "Swish", "Mish", "Sigmoid", "ELU", "AbsVal", "BNLL", "Power"); + } + + static testing::internal::ParamGenerator > dnnBackendsAndTargetsForFusionTests() + { + return dnnBackendsAndTargets(false, false, true, false); // OCV OpenCL + OCV CPU + } +}; + +typedef TestWithParam > > ConvolutionActivationFusion; +TEST_P(ConvolutionActivationFusion, Accuracy) +{ + // input + // | + // ----------------------- + // | convolution | + // ----------------------- + // | + // ----------------------- + // | activation | + // ----------------------- + // | + // output + + const int batch_size = 2, in_channels = 16; + const int in_height = 16, in_width = 16; + int inputShape[] = {batch_size, in_channels, in_height, in_width}; + Mat input(4, &inputShape[0], CV_32F); + randu(input, 1.0f, 2.0f); + + bool bias_term = get<0>(GetParam()); + LayerParams convParams; + TestLayerFusion::makeDefaultTestConvolutionLayer(convParams, in_channels, in_channels, bias_term); + + std::string actType = get<1>(GetParam()); + LayerParams activationParams; + TestLayerFusion::makeDefaultTestActivationLayer(activationParams, actType, in_channels); + + Backend backendId = get<0>(get<2>(GetParam())); + Target targetId = get<1>(get<2>(GetParam())); + + // bug: https://github.com/opencv/opencv/issues/17964 + if (actType == "Power" && backendId == DNN_BACKEND_OPENCV && (targetId == DNN_TARGET_OPENCL || targetId == DNN_TARGET_OPENCL_FP16)) + applyTestTag(CV_TEST_TAG_DNN_SKIP_OPENCL); + + // bug: https://github.com/opencv/opencv/issues/17953 + if (actType == "ChannelsPReLU" && bias_term == false && + backendId == DNN_BACKEND_OPENCV && (targetId == DNN_TARGET_OPENCL || targetId == DNN_TARGET_OPENCL_FP16)) + { + applyTestTag(CV_TEST_TAG_DNN_SKIP_OPENCL); + } + + Net net; + int convId = net.addLayer(convParams.name, convParams.type, convParams); + int activId = net.addLayerToPrev(activationParams.name, activationParams.type, activationParams); + net.connect(0, 0, convId, 0); + + std::vector expectedFusedLayers; + if (backendId == DNN_BACKEND_OPENCV) + { + if (targetId == DNN_TARGET_CPU) + expectedFusedLayers.push_back(activId); // all activations are fused + else if (targetId == DNN_TARGET_OPENCL || targetId == DNN_TARGET_OPENCL_FP16) + { + if (actType == "ReLU" || actType == "ChannelsPReLU" || actType == "ReLU6" || actType == "TanH" || actType == "Power") + expectedFusedLayers.push_back(activId); + } + } + + TestLayerFusion::test(input, net, backendId, targetId, expectedFusedLayers); +} +INSTANTIATE_TEST_CASE_P(TestLayerFusion, ConvolutionActivationFusion, Combine( +/* bias */ testing::Bool(), +/* activation */ TestLayerFusion::activationLayersList(), + TestLayerFusion::dnnBackendsAndTargetsForFusionTests() +)); + +typedef TestWithParam > > ConvolutionEltwiseFusion; +TEST_P(ConvolutionEltwiseFusion, Accuracy) +{ + // input + // | + // ------------------------------- + // | | + // | --------------- + // | | convolution | + // | --------------- + // | | + // | ---------------- | + // --------| eltwise op |------- + // ---------------- + // | + // output + + const int batch_size = 2, in_channels = 16; + const int in_height = 16, in_width = 16; + int inputShape[] = {batch_size, in_channels, in_height, in_width}; + Mat input(4, &inputShape[0], CV_32F); + randu(input, 1.0f, 2.0f); // avoid small values to test eltwise div + + bool bias_term = get<0>(GetParam()); + LayerParams convParams; + TestLayerFusion::makeDefaultTestConvolutionLayer(convParams, in_channels, in_channels, bias_term); + + std::string eltwiseOp = get<1>(GetParam()); + bool weightedEltwise = get<2>(GetParam()); + if (eltwiseOp != "sum" && weightedEltwise) + throw SkipTestException("weighted eltwise not supported"); + LayerParams eltwiseParams; + TestLayerFusion::makeDefaultTestEltwiseLayer(eltwiseParams, eltwiseOp, weightedEltwise); + + Net net; + int convId = net.addLayer(convParams.name, convParams.type, convParams); + int eltwiseId = net.addLayer(eltwiseParams.name, eltwiseParams.type, eltwiseParams); + net.connect(0, 0, convId, 0); + net.connect(convId, 0, eltwiseId, 0); + net.connect(0, 0, eltwiseId, 1); + + Backend backendId = get<0>(get<3>(GetParam())); + Target targetId = get<1>(get<3>(GetParam())); + TestLayerFusion::test(input, net, backendId, targetId); +} +INSTANTIATE_TEST_CASE_P(TestLayerFusion, ConvolutionEltwiseFusion, Combine( +/* bias */ testing::Bool(), +/* eltwise op */ TestLayerFusion::eltwiseOpList(), +/* eltwise weighted */ testing::Bool(), + TestLayerFusion::dnnBackendsAndTargetsForFusionTests() +)); + +typedef TestWithParam > > ConvolutionEltwiseActivationFusion; +TEST_P(ConvolutionEltwiseActivationFusion, Accuracy) +{ + // input + // | + // ------------------------------- + // | | + // | --------------- + // | | convolution | + // | --------------- + // | | + // | ---------------- | + // --------| eltwise op |------- + // ---------------- + // | + // ---------------- + // | activation | + // ---------------- + // | + // output + + const int batch_size = 2, in_channels = 16; + const int in_height = 16, in_width = 16; + int inputShape[] = {batch_size, in_channels, in_height, in_width}; + Mat input(4, &inputShape[0], CV_32F); + randu(input, 1.0f, 2.0f); // avoid small values to test eltwise div + + bool bias_term = get<0>(GetParam()); + LayerParams convParams; + TestLayerFusion::makeDefaultTestConvolutionLayer(convParams, in_channels, in_channels, bias_term); + + std::string eltwiseOp = get<1>(GetParam()); + bool weightedEltwise = get<2>(GetParam()); + if (eltwiseOp != "sum" && weightedEltwise) + throw SkipTestException("weighted eltwise not supported"); + LayerParams eltwiseParams; + TestLayerFusion::makeDefaultTestEltwiseLayer(eltwiseParams, eltwiseOp, false); + + std::string actType = get<3>(GetParam()); + LayerParams activationParams; + TestLayerFusion::makeDefaultTestActivationLayer(activationParams, actType, in_channels); + + Backend backendId = get<0>(get<4>(GetParam())); + Target targetId = get<1>(get<4>(GetParam())); + + // bug: https://github.com/opencv/opencv/issues/17945 + if (eltwiseOp != "sum" && backendId == DNN_BACKEND_OPENCV && (targetId == DNN_TARGET_OPENCL || targetId == DNN_TARGET_OPENCL_FP16)) + applyTestTag(CV_TEST_TAG_DNN_SKIP_OPENCL); + + // bug: https://github.com/opencv/opencv/issues/17953 + if (eltwiseOp == "sum" && actType == "ChannelsPReLU" && bias_term == false && + backendId == DNN_BACKEND_OPENCV && (targetId == DNN_TARGET_OPENCL || targetId == DNN_TARGET_OPENCL_FP16)) + { + applyTestTag(CV_TEST_TAG_DNN_SKIP_OPENCL); + } + + // bug: https://github.com/opencv/opencv/issues/17964 + if (actType == "Power" && backendId == DNN_BACKEND_OPENCV && (targetId == DNN_TARGET_OPENCL || targetId == DNN_TARGET_OPENCL_FP16)) + applyTestTag(CV_TEST_TAG_DNN_SKIP_OPENCL); + + Net net; + int convId = net.addLayer(convParams.name, convParams.type, convParams); + int eltwiseId = net.addLayer(eltwiseParams.name, eltwiseParams.type, eltwiseParams); + int activId = net.addLayer(activationParams.name, activationParams.type, activationParams); + net.connect(0, 0, convId, 0); + net.connect(convId, 0, eltwiseId, 0); + net.connect(0, 0, eltwiseId, 1); + net.connect(eltwiseId, 0, activId, 0); + + std::vector expectedFusedLayers; + if (backendId == DNN_BACKEND_OPENCV) + { + if (targetId == DNN_TARGET_CPU) + expectedFusedLayers.push_back(activId); // activation is fused with eltwise layer + else if (targetId == DNN_TARGET_OPENCL || targetId == DNN_TARGET_OPENCL_FP16) + { + if (actType == "ReLU" || actType == "ChannelsPReLU" || actType == "Power") + { + expectedFusedLayers.push_back(eltwiseId); + expectedFusedLayers.push_back(activId); + } + } + } + + TestLayerFusion::test(input, net, backendId, targetId, expectedFusedLayers); +} +INSTANTIATE_TEST_CASE_P(TestLayerFusion, ConvolutionEltwiseActivationFusion, Combine( +/* bias */ testing::Bool(), +/* eltwise op */ TestLayerFusion::eltwiseOpList(), +/* eltwise weighted */ testing::Bool(), +/* activation */ TestLayerFusion::activationLayersList(), + TestLayerFusion::dnnBackendsAndTargetsForFusionTests() +)); + +typedef TestWithParam > > ConvolutionActivationEltwiseFusion; +TEST_P(ConvolutionActivationEltwiseFusion, Accuracy) +{ + // input + // | + // ------------------------------- + // | | + // | ---------------- + // | | convolution | + // | ---------------- + // | | + // | ---------------- + // | | activation | + // | ---------------- + // | | + // | ---------------- | + // --------| eltwise sum |------- + // ---------------- + // | + + const int batch_size = 2, in_channels = 16; + const int in_height = 16, in_width = 16; + int inputShape[] = {batch_size, in_channels, in_height, in_width}; + Mat input(4, &inputShape[0], CV_32F); + randu(input, 1.0f, 2.0f); // avoid small values to test eltwise div + + bool bias_term = get<0>(GetParam()); + LayerParams convParams; + TestLayerFusion::makeDefaultTestConvolutionLayer(convParams, in_channels, in_channels, bias_term); + + std::string actType = get<1>(GetParam()); + LayerParams activationParams; + TestLayerFusion::makeDefaultTestActivationLayer(activationParams, actType, in_channels); + + std::string eltwiseOp = get<2>(GetParam()); + bool weightedEltwise = get<3>(GetParam()); + if (eltwiseOp != "sum" && weightedEltwise) + throw SkipTestException("weighted eltwise not supported"); + LayerParams eltwiseParams; + TestLayerFusion::makeDefaultTestEltwiseLayer(eltwiseParams, eltwiseOp, false); + + Backend backendId = get<0>(get<4>(GetParam())); + Target targetId = get<1>(get<4>(GetParam())); + + // bug: https://github.com/opencv/opencv/issues/17964 + if (actType == "Power" && backendId == DNN_BACKEND_OPENCV && (targetId == DNN_TARGET_OPENCL || targetId == DNN_TARGET_OPENCL_FP16)) + applyTestTag(CV_TEST_TAG_DNN_SKIP_OPENCL); + + // bug: https://github.com/opencv/opencv/issues/17953 + if (actType == "ChannelsPReLU" && bias_term == false && + backendId == DNN_BACKEND_OPENCV && (targetId == DNN_TARGET_OPENCL || targetId == DNN_TARGET_OPENCL_FP16)) + { + applyTestTag(CV_TEST_TAG_DNN_SKIP_OPENCL); + } + + Net net; + int convId = net.addLayer(convParams.name, convParams.type, convParams); + int activId = net.addLayer(activationParams.name, activationParams.type, activationParams); + int eltwiseId = net.addLayer(eltwiseParams.name, eltwiseParams.type, eltwiseParams); + net.connect(0, 0, convId, 0); + net.connect(convId, 0, activId, 0); + net.connect(activId, 0, eltwiseId, 0); + net.connect(0, 0, eltwiseId, 1); + + std::vector expectedFusedLayers; + if (backendId == DNN_BACKEND_OPENCV) + { + if (targetId == DNN_TARGET_CPU) + expectedFusedLayers.push_back(activId); // activation fused with convolution + else if (targetId == DNN_TARGET_OPENCL || targetId == DNN_TARGET_OPENCL_FP16) + { + if (actType == "ReLU" || actType == "ChannelsPReLU" || actType == "ReLU6" || actType == "TanH" || actType == "Power") + expectedFusedLayers.push_back(activId); // activation fused with convolution + } + } + + TestLayerFusion::test(input, net, backendId, targetId, expectedFusedLayers); +} +INSTANTIATE_TEST_CASE_P(TestLayerFusion, ConvolutionActivationEltwiseFusion, Combine( +/* bias */ testing::Bool(), +/* activation */ TestLayerFusion::activationLayersList(), +/* eltwise op */ TestLayerFusion::eltwiseOpList(), +/* eltwise weighted */ testing::Bool(), + TestLayerFusion::dnnBackendsAndTargetsForFusionTests() +)); + }} // namespace