From f059d3b51723c356d6ab26cae0db59cd5d74f3d6 Mon Sep 17 00:00:00 2001 From: Abhishek Gola Date: Fri, 13 Mar 2026 20:57:14 +0530 Subject: [PATCH] Merge pull request #28634 from abhishek-gola:flops_addition Added getFLOPS support in new DNN engine #28634 closes: https://github.com/opencv/opencv/issues/26199 ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake --- modules/dnn/perf/perf_net.cpp | 4 +- modules/dnn/src/layers/attention_layer.cpp | 28 + .../dnn/src/layers/attention_onnxai_layer.cpp | 21 + modules/dnn/src/layers/einsum_layer.cpp | 15 + modules/dnn/src/layers/gemm_layer.cpp | 21 + modules/dnn/src/layers/matmul_layer.cpp | 23 + modules/dnn/src/layers/recurrent2_layers.cpp | 18 + modules/dnn/src/layers/reshape2_layer.cpp | 18 +- modules/dnn/src/net_impl.cpp | 105 ++++ modules/dnn/src/net_impl.hpp | 3 + modules/dnn/test/test_getflops.cpp | 542 ++++++++++++++++++ 11 files changed, 786 insertions(+), 12 deletions(-) create mode 100644 modules/dnn/test/test_getflops.cpp diff --git a/modules/dnn/perf/perf_net.cpp b/modules/dnn/perf/perf_net.cpp index 01cbd74c3b..3c148c8cae 100644 --- a/modules/dnn/perf/perf_net.cpp +++ b/modules/dnn/perf/perf_net.cpp @@ -65,9 +65,7 @@ public: size_t weightsMemory = 0, blobsMemory = 0; net.getMemoryConsumption(netMatShapes, netMatTypes, weightsMemory, blobsMemory); int64 flops = net.getFLOPS(netMatShapes, netMatTypes); - // [TODO] implement getFLOPS in the new engine - // Issue: https://github.com/opencv/opencv/issues/26199 - CV_Assert(flops > 0 || net.getMainGraph()); + CV_Assert(flops > 0); std::cout << "Memory consumption:" << std::endl; std::cout << " Weights(parameters): " << divUp(weightsMemory, 1u<<20) << " Mb" << std::endl; std::cout << " Blobs: " << divUp(blobsMemory, 1u<<20) << " Mb" << std::endl; diff --git a/modules/dnn/src/layers/attention_layer.cpp b/modules/dnn/src/layers/attention_layer.cpp index 16b343dc8d..807b3e2bda 100644 --- a/modules/dnn/src/layers/attention_layer.cpp +++ b/modules/dnn/src/layers/attention_layer.cpp @@ -212,6 +212,34 @@ class AttentionLayerImpl CV_FINAL : public AttentionLayer { return false; } + virtual int64 getFLOPS(const std::vector &inputs, + const std::vector &outputs) const CV_OVERRIDE + { + const auto &input_shape = inputs[0]; + int64 B = input_shape[0]; + int64 S = input_shape[1]; + int64 D = input_shape[2]; + + const auto &weight_shape = blobs.empty() ? inputs[1] : shape(blobs.front()); + int64 hidden = weight_shape[1]; + + int64 q_size = (int64)qkv_hidden_sizes[0]; + int64 k_size = (int64)qkv_hidden_sizes[1]; + int64 v_size = hidden - q_size - k_size; + int64 q_head = (int64)qkv_head_sizes[0]; + int64 v_head = v_size / (int64)num_heads; + + // Input projection: Q, K, V = input * W + b + int64 flops = B * S * (CV_BIG_INT(2) * D * hidden); + // QK^T: (B*num_heads) * S * S * q_head_size + flops += B * (int64)num_heads * CV_BIG_INT(2) * S * S * q_head; + // Softmax: ~4 ops per element + flops += B * (int64)num_heads * 4 * S * S; + // Attention * V: (B*num_heads) * S * v_head_size * S + flops += B * (int64)num_heads * CV_BIG_INT(2) * S * v_head * S; + return flops; + } + virtual void finalize(InputArrayOfArrays inputs_arr, OutputArrayOfArrays outputs_arr) CV_OVERRIDE { opt.init(); diff --git a/modules/dnn/src/layers/attention_onnxai_layer.cpp b/modules/dnn/src/layers/attention_onnxai_layer.cpp index b9fb62fdce..209e70242e 100644 --- a/modules/dnn/src/layers/attention_onnxai_layer.cpp +++ b/modules/dnn/src/layers/attention_onnxai_layer.cpp @@ -119,6 +119,27 @@ class AttentionOnnxAiLayerImpl CV_FINAL : public AttentionOnnxAiLayer { return false; } + virtual int64 getFLOPS(const std::vector &inputs, + const std::vector &outputs) const CV_OVERRIDE + { + const int input_dims = inputs[0].dims; + int64 B = inputs[0][0]; + int64 Sq = inputs[0][input_dims - 2]; + int64 Skv = inputs[1][input_dims - 2]; + int64 nhq = input_dims == 4 ? inputs[0][1] : q_num_heads; + int64 qk_head = input_dims == 4 ? inputs[0][3] : inputs[0][2] / nhq; + int64 nhkv = input_dims == 4 ? inputs[1][1] : kv_num_heads; + int64 v_head = input_dims == 4 ? inputs[2][3] : inputs[2][2] / nhkv; + + // QK^T: batch * nhq * (2 * Sq * Skv * qk_head) + int64 flops = B * nhq * CV_BIG_INT(2) * Sq * Skv * qk_head; + // Softmax: ~4 ops per element + flops += B * nhq * 4 * Sq * Skv; + // Attention * V: batch * nhq * (2 * Sq * v_head * Skv) + flops += B * nhq * CV_BIG_INT(2) * Sq * v_head * Skv; + return flops; + } + void forward(InputArrayOfArrays inputs_arr, OutputArrayOfArrays outputs_arr, OutputArrayOfArrays internals_arr) CV_OVERRIDE { opt.init(); diff --git a/modules/dnn/src/layers/einsum_layer.cpp b/modules/dnn/src/layers/einsum_layer.cpp index b6fe56e127..b3a2ce09d7 100644 --- a/modules/dnn/src/layers/einsum_layer.cpp +++ b/modules/dnn/src/layers/einsum_layer.cpp @@ -492,6 +492,21 @@ public: return true; } // getMemoryShape + virtual int64 getFLOPS(const std::vector &inputs, + const std::vector &outputs) const CV_OVERRIDE + { + computeOutputShape(inputs); + + int64 totalProduct = 1; + for (int i = 0; i < numLetterIndices; i++) { + int dimVal = subscriptIndicesToDimValue[i]; + if (dimVal > 0) + totalProduct *= dimVal; + } + // 2 FLOPs per multiply-add in the contraction + return CV_BIG_INT(2) * totalProduct; + } + // forward void forward(InputArrayOfArrays inputs_arr, OutputArrayOfArrays outputs_arr, diff --git a/modules/dnn/src/layers/gemm_layer.cpp b/modules/dnn/src/layers/gemm_layer.cpp index 8f9e698065..ceb12f0c8a 100644 --- a/modules/dnn/src/layers/gemm_layer.cpp +++ b/modules/dnn/src/layers/gemm_layer.cpp @@ -170,6 +170,27 @@ public: return false; } + virtual int64 getFLOPS(const std::vector &inputs, + const std::vector &outputs) const CV_OVERRIDE + { + CV_Assert(!inputs.empty()); + LayerGemmOpMode mode_ = getOpMode(inputs.size(), blobs.size()); + const auto shape_A = inputs[0]; + const auto shape_B = constB(mode_) ? shape(blobs[0]) : inputs[1]; + int M = trans_a ? shape_A.back() : shape_A[shape_A.size() - 2]; + int K = trans_a ? shape_A[shape_A.size() - 2] : shape_A.back(); + int N = trans_b ? shape_B[shape_B.size() - 2] : shape_B.back(); + + int64 batches = std::accumulate(shape_A.begin(), shape_A.end() - 2, + CV_BIG_INT(1), std::multiplies()); + + // 2*M*N*K multiply-adds, +M*N for bias + int64 flops = batches * (CV_BIG_INT(2) * M * N * K); + if (have_bias) + flops += batches * M * N; + return flops; + } + // TODO: replace with cv::broadcast() once 1d mat is supported // FIXME: fix if conditions if 1d mat is supported properly void broadcastCWtihBeta(int M, int N, const Mat &C) { diff --git a/modules/dnn/src/layers/matmul_layer.cpp b/modules/dnn/src/layers/matmul_layer.cpp index 4d232b7a3e..10463f1440 100644 --- a/modules/dnn/src/layers/matmul_layer.cpp +++ b/modules/dnn/src/layers/matmul_layer.cpp @@ -118,6 +118,29 @@ class MatMulLayerImpl CV_FINAL : public MatMulLayer { return false; } + virtual int64 getFLOPS(const std::vector &inputs, + const std::vector &outputs) const CV_OVERRIDE + { + CV_Assert(!inputs.empty()); + const auto shape_A = inputs[0], shape_B = blobs.empty() ? inputs[1] : shape(blobs[0]); + int mA = shape_A[shape_A.size() - 2], nA = shape_A.back(); + int mB = shape_B[shape_B.size() - 2], nB = shape_B.back(); + int M = trans_a ? nA : mA; + int N = trans_b ? mB : nB; + int K = trans_a ? mA : nA; + + int64 batch = 1; + for (size_t i = 0; i + 2 < outputs[0].size(); i++) + batch *= outputs[0][i]; + + // 2*M*N*K multiply-adds per batch element, +M*N for bias if present + int64 flops = batch * (CV_BIG_INT(2) * M * N * K); + int num_inputs = (int)inputs.size() + (int)blobs.size(); + if (num_inputs == 3) + flops += batch * M * N; + return flops; + } + virtual void finalize(InputArrayOfArrays inputs_arr, OutputArrayOfArrays outputs_arr) CV_OVERRIDE { opt.init(); diff --git a/modules/dnn/src/layers/recurrent2_layers.cpp b/modules/dnn/src/layers/recurrent2_layers.cpp index f981aceb82..da26bc2285 100644 --- a/modules/dnn/src/layers/recurrent2_layers.cpp +++ b/modules/dnn/src/layers/recurrent2_layers.cpp @@ -194,6 +194,24 @@ class LSTM2LayerImpl CV_FINAL : public LSTM2Layer internals.assign(4, inputs[0]); } + virtual int64 getFLOPS(const std::vector &inputs, + const std::vector &outputs) const CV_OVERRIDE + { + // LSTM: 4 gates, each gate = input_size*hidden_size + hidden_size*hidden_size MACs + const MatShape& inp0 = inputs[0]; + const MatShape& Wx = inputs[1]; + int _hidSize = numHidden; + int _inpSize = Wx[2]; + int _seqLen = inp0[0]; + int _batchSize = inp0[1]; + int numDirs = 1 + static_cast(bidirectional); + + // Per timestep: 4 gates * (2*input_size*hidden_size + 2*hidden_size*hidden_size + hidden_size) + int64 flopsPerStep = CV_BIG_INT(4) * (CV_BIG_INT(2) * _inpSize * _hidSize + + CV_BIG_INT(2) * _hidSize * _hidSize + + _hidSize); + return (int64)numDirs * _seqLen * _batchSize * flopsPerStep; + } void forward(InputArrayOfArrays inputs_arr, OutputArrayOfArrays outputs_arr, diff --git a/modules/dnn/src/layers/reshape2_layer.cpp b/modules/dnn/src/layers/reshape2_layer.cpp index 081d9a739e..3fad397c9c 100644 --- a/modules/dnn/src/layers/reshape2_layer.cpp +++ b/modules/dnn/src/layers/reshape2_layer.cpp @@ -52,21 +52,21 @@ public: int i, ndims = shapeParam.size(); newShapeDesc.resize(ndims); for (i = 0; i < ndims; i++) { - int sz = shapeParam.get(i); - if (sz <= 0) - dynamicShapeSpec = true; - newShapeDesc[i] = sz; + newShapeDesc[i] = shapeParam.get(i); } } } virtual bool dynamicOutputShapes() const CV_OVERRIDE { - // [TODO] fix. If the 'shape' spec is attribute, - // or if shape is a constant 2nd input of the layer, - // then the output shape can be inferred from the input tensor shape. - // That is, dynamicShapeSpec is not quite incorrect. - return dynamicShapeSpec; + if (!dynamicShapeSpec) + return false; + // When the shape comes from a 2nd input, check if it's a constant tensor. + // If so, the output shape can still be inferred. + Net::Impl* netimpl_ = getNetImpl(this); + if (netimpl_ && this->inputs.size() == 2) + return !netimpl_->isConstArg(this->inputs[1]); + return true; } virtual bool supportBackend(int backendId) CV_OVERRIDE diff --git a/modules/dnn/src/net_impl.cpp b/modules/dnn/src/net_impl.cpp index cece540a16..b4039a7541 100644 --- a/modules/dnn/src/net_impl.cpp +++ b/modules/dnn/src/net_impl.cpp @@ -2375,9 +2375,71 @@ std::vector Net::Impl::getUnconnectedOutLayersNames() /*const*/ } +int64 Net::Impl::getFLOPSGraph(const Ptr& graph, + const std::vector& shapeCache, + const std::vector& typeCache) const +{ + if (!graph) + return 0; + + int64 flops = 0; + const std::vector>& prog = graph->prog(); + + for (const Ptr& layer : prog) { + if (!layer) + continue; + + const std::vector& inputs = layer->inputs; + const std::vector& outputs = layer->outputs; + int ninputs = (int)inputs.size(); + int noutputs = (int)outputs.size(); + + std::vector inpShapes(ninputs), outShapes(noutputs); + for (int i = 0; i < ninputs; i++) { + Arg inp = inputs[i]; + const ArgData& adata = args.at(inp.idx); + if (adata.kind == DNN_ARG_CONST || adata.kind == DNN_ARG_EMPTY) + inpShapes[i] = adata.shape; + else + inpShapes[i] = shapeCache[inp.idx]; + } + for (int i = 0; i < noutputs; i++) { + Arg out = outputs[i]; + if (out.idx > 0 && out.idx < (int)shapeCache.size()) + outShapes[i] = shapeCache[out.idx]; + } + + // Skip FLOPS calculation if any shape is empty (unknown due to dynamic shapes) + bool hasEmptyShape = false; + for (int i = 0; i < ninputs && !hasEmptyShape; i++) + hasEmptyShape = inpShapes[i].empty(); + for (int i = 0; i < noutputs && !hasEmptyShape; i++) + hasEmptyShape = outShapes[i].empty(); + + if (!hasEmptyShape) + flops += layer->getFLOPS(inpShapes, outShapes); + + const std::vector>* subgraphs = layer->subgraphs(); + if (subgraphs) { + for (const Ptr& sg : *subgraphs) + flops += getFLOPSGraph(sg, shapeCache, typeCache); + } + } + return flops; +} + + int64 Net::Impl::getFLOPS(const std::vector& netInputShapes, const std::vector& netInputTypes) /*const*/ { + if (mainGraph) { + LayerShapes shapes; + std::vector shapeCache; + std::vector typeCache; + tryInferShapes(netInputShapes, netInputTypes, shapes, shapeCache, typeCache); + return getFLOPSGraph(mainGraph, shapeCache, typeCache); + } + int64 flops = 0; std::vector ids; std::vector> inShapes, outShapes; @@ -2399,6 +2461,49 @@ int64 Net::Impl::getFLOPS( const std::vector& netInputShapes, const std::vector& netInputTypes) /*const*/ { + if (mainGraph) { + LayerShapes shapes; + std::vector shapeCache; + std::vector typeCache; + tryInferShapes(netInputShapes, netInputTypes, shapes, shapeCache, typeCache); + + CV_Assert(0 <= layerId && layerId < (int)totalLayers); + int localIdx = layerId; + for (const Ptr& graph : allgraphs) { + int progSize = (int)graph->prog().size(); + if (localIdx < progSize) { + const Ptr& layer = graph->prog()[localIdx]; + if (!layer) + return 0; + + const std::vector& inputs = layer->inputs; + const std::vector& outputs = layer->outputs; + int ninputs = (int)inputs.size(); + int noutputs = (int)outputs.size(); + + std::vector inpShapes(ninputs), outShapes(noutputs); + for (int i = 0; i < ninputs; i++) { + Arg inp = inputs[i]; + const ArgData& adata = args.at(inp.idx); + if (adata.kind == DNN_ARG_CONST || adata.kind == DNN_ARG_EMPTY) + inpShapes[i] = adata.shape; + else + inpShapes[i] = shapeCache[inp.idx]; + } + for (int i = 0; i < noutputs; i++) { + Arg out = outputs[i]; + if (out.idx > 0 && out.idx < (int)shapeCache.size()) + outShapes[i] = shapeCache[out.idx]; + } + + return layer->getFLOPS(inpShapes, outShapes); + } + localIdx -= progSize; + } + + CV_Error(Error::StsOutOfRange, format("Layer id %d is out of range", layerId)); + } + Impl::MapIdToLayerData::const_iterator layer = layers.find(layerId); CV_Assert(layer != layers.end()); diff --git a/modules/dnn/src/net_impl.hpp b/modules/dnn/src/net_impl.hpp index d37fe79d35..a9d961fb02 100644 --- a/modules/dnn/src/net_impl.hpp +++ b/modules/dnn/src/net_impl.hpp @@ -305,6 +305,9 @@ struct Net::Impl : public detail::NetImplBase const int layerId, const std::vector& netInputShapes, const std::vector& netInputTypes) /*const*/; + int64 getFLOPSGraph(const Ptr& graph, + const std::vector& shapeCache, + const std::vector& typeCache) const; void getMemoryConsumption( const int layerId, diff --git a/modules/dnn/test/test_getflops.cpp b/modules/dnn/test/test_getflops.cpp new file mode 100644 index 0000000000..027657ae89 --- /dev/null +++ b/modules/dnn/test/test_getflops.cpp @@ -0,0 +1,542 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// Copyright (C) 2025, BigVision LLC, all rights reserved. +// Third party copyrights are property of their respective owners. + +#include "test_precomp.hpp" +#include +#include + +namespace opencv_test { namespace { + +//build a single-layer network +static Net buildSingleLayerNet(LayerParams& lp, const MatShape& inputShape, + int inputType = CV_32F) +{ + Net net; + net.addLayerToPrev(lp.name, lp.type, lp); + Mat input(inputShape, inputType); + randu(input, -1, 1); + net.setInput(input); + net.setPreferableBackend(DNN_BACKEND_OPENCV); + return net; +} + +TEST(Test_GetFLOPS, Convolution) +{ + LayerParams lp; + lp.type = "Convolution"; + lp.name = "conv"; + lp.set("kernel_size", 3); + lp.set("num_output", 64); + lp.set("pad", 1); + lp.set("bias_term", true); + + int weightsShape[] = {64, 3, 3, 3}; + Mat weights(4, weightsShape, CV_32F); + randu(weights, -1, 1); + lp.blobs.push_back(weights); + Mat bias(1, 64, CV_32F, Scalar(0)); + lp.blobs.push_back(bias); + + MatShape inputShape{1, 3, 224, 224}; + Net net = buildSingleLayerNet(lp, inputShape); + + int64 flops = net.getFLOPS(inputShape, CV_32F); + + // Expected: output is [1, 64, 224, 224] + // FLOPS per output element = 2 * 3*3*3 + 1 = 55 + // Total = 1 * 64 * 224 * 224 * 55 = 176,455,680 + // But the Data layer also contributes 0 flops, so total = conv flops + int64 expectedFlops = (int64)1 * 64 * 224 * 224 * (2 * 3 * 3 * 3 + 1); + EXPECT_EQ(flops, expectedFlops); +} + +TEST(Test_GetFLOPS, FullyConnected) +{ + LayerParams lp; + lp.type = "InnerProduct"; + lp.name = "fc"; + lp.set("num_output", 1000); + + int weightsShape[] = {1000, 2048}; + Mat weights(2, weightsShape, CV_32F); + randu(weights, -1, 1); + lp.blobs.push_back(weights); + Mat bias(1, 1000, CV_32F, Scalar(0)); + lp.blobs.push_back(bias); + + MatShape inputShape{1, 2048}; + Net net = buildSingleLayerNet(lp, inputShape); + + int64 flops = net.getFLOPS(inputShape, CV_32F); + + // Expected: 3 * innerSize * output = 3 * 2048 * 1000 = 6,144,000 + int64 expectedFlops = (int64)3 * 2048 * 1000; + EXPECT_EQ(flops, expectedFlops); +} + +TEST(Test_GetFLOPS, MaxPooling) +{ + LayerParams lp; + lp.type = "Pooling"; + lp.name = "pool"; + lp.set("pool", "max"); + lp.set("kernel_size", 2); + lp.set("stride", 2); + + MatShape inputShape{1, 64, 112, 112}; + Net net = buildSingleLayerNet(lp, inputShape); + + int64 flops = net.getFLOPS(inputShape, CV_32F); + + // Output: [1, 64, 56, 56] + // Max pool: karea comparisons per output element = 2*2 = 4 + // Total = 1 * 64 * 56 * 56 * 4 = 802,816 + int64 expectedFlops = (int64)1 * 64 * 56 * 56 * 4; + EXPECT_EQ(flops, expectedFlops); +} + +TEST(Test_GetFLOPS, BatchNorm) +{ + LayerParams lp; + lp.type = "BatchNorm"; + lp.name = "bn"; + lp.set("has_weight", true); + lp.set("has_bias", true); + lp.set("eps", 1e-5); + + int channels = 64; + Mat mean(1, channels, CV_32F, Scalar(0)); + Mat var(1, channels, CV_32F, Scalar(1)); + Mat scale(1, channels, CV_32F, Scalar(1)); + Mat shift(1, channels, CV_32F, Scalar(0)); + lp.blobs.push_back(mean); + lp.blobs.push_back(var); + lp.blobs.push_back(scale); + lp.blobs.push_back(shift); + + MatShape inputShape{1, 64, 56, 56}; + Net net = buildSingleLayerNet(lp, inputShape); + + int64 flops = net.getFLOPS(inputShape, CV_32F); + + // BatchNorm: 3 flops per element + int64 expectedFlops = (int64)3 * 1 * 64 * 56 * 56; + EXPECT_EQ(flops, expectedFlops); +} + +TEST(Test_GetFLOPS, Softmax) +{ + LayerParams lp; + lp.type = "Softmax"; + lp.name = "softmax"; + + MatShape inputShape{1, 1000}; + Net net = buildSingleLayerNet(lp, inputShape); + + int64 flops = net.getFLOPS(inputShape, CV_32F); + + // Softmax: 4 flops per element + int64 expectedFlops = (int64)4 * 1000; + EXPECT_EQ(flops, expectedFlops); +} + +TEST(Test_GetFLOPS, Scale) +{ + LayerParams lp; + lp.type = "Scale"; + lp.name = "scale"; + lp.set("axis", 1); + lp.set("has_bias", true); + + int channels = 64; + Mat scaleData(1, channels, CV_32F, Scalar(1)); + Mat biasData(1, channels, CV_32F, Scalar(0)); + lp.blobs.push_back(scaleData); + lp.blobs.push_back(biasData); + + MatShape inputShape{1, 64, 56, 56}; + Net net = buildSingleLayerNet(lp, inputShape); + + int64 flops = net.getFLOPS(inputShape, CV_32F); + + // Scale: 2 flops per element (multiply + add) + int64 expectedFlops = (int64)2 * 1 * 64 * 56 * 56; + EXPECT_EQ(flops, expectedFlops); +} + +TEST(Test_GetFLOPS, MultiLayerNetwork) +{ + // Build a small network: Conv -> BatchNorm -> Pooling + Net net; + + // Conv layer + { + LayerParams lp; + lp.type = "Convolution"; + lp.name = "conv1"; + lp.set("kernel_size", 3); + lp.set("num_output", 16); + lp.set("pad", 1); + lp.set("bias_term", true); + + int wShape[] = {16, 3, 3, 3}; + Mat w(4, wShape, CV_32F); + randu(w, -1, 1); + lp.blobs.push_back(w); + Mat b(1, 16, CV_32F, Scalar(0)); + lp.blobs.push_back(b); + net.addLayerToPrev(lp.name, lp.type, lp); + } + + // BatchNorm + { + LayerParams lp; + lp.type = "BatchNorm"; + lp.name = "bn1"; + lp.set("has_weight", true); + lp.set("has_bias", true); + lp.set("eps", 1e-5); + + Mat mean(1, 16, CV_32F, Scalar(0)); + Mat var(1, 16, CV_32F, Scalar(1)); + Mat scale(1, 16, CV_32F, Scalar(1)); + Mat shift(1, 16, CV_32F, Scalar(0)); + lp.blobs.push_back(mean); + lp.blobs.push_back(var); + lp.blobs.push_back(scale); + lp.blobs.push_back(shift); + net.addLayerToPrev(lp.name, lp.type, lp); + } + + // MaxPool + { + LayerParams lp; + lp.type = "Pooling"; + lp.name = "pool1"; + lp.set("pool", "max"); + lp.set("kernel_size", 2); + lp.set("stride", 2); + net.addLayerToPrev(lp.name, lp.type, lp); + } + + MatShape inputShape{1, 3, 32, 32}; + Mat input(inputShape, CV_32F); + randu(input, -1, 1); + net.setInput(input); + net.setPreferableBackend(DNN_BACKEND_OPENCV); + + int64 flops = net.getFLOPS(inputShape, CV_32F); + + // Conv: output [1,16,32,32], flops = 1*16*32*32*(2*3*3*3+1) = 903,168 + int64 convFlops = (int64)1 * 16 * 32 * 32 * (2 * 3 * 3 * 3 + 1); + // BN: 3 * 1*16*32*32 = 49,152 + int64 bnFlops = (int64)3 * 1 * 16 * 32 * 32; + // Pool: output [1,16,16,16], karea=4, flops = 1*16*16*16*4 = 16,384 + int64 poolFlops = (int64)1 * 16 * 16 * 16 * 4; + + int64 expectedFlops = convFlops + bnFlops + poolFlops; + EXPECT_EQ(flops, expectedFlops); +} + +TEST(Test_GetFLOPS, EmptyNet) +{ + Net net; + MatShape inputShape{1, 3, 224, 224}; + // An empty net should not crash + EXPECT_NO_THROW(net.getFLOPS(inputShape, CV_32F)); +} + +TEST(Test_GetFLOPS, PerLayerFLOPS) +{ + // Test getFLOPS with specific layerId + Net net; + + // Conv layer + { + LayerParams lp; + lp.type = "Convolution"; + lp.name = "conv1"; + lp.set("kernel_size", 3); + lp.set("num_output", 8); + lp.set("pad", 1); + lp.set("bias_term", true); + + int wShape[] = {8, 3, 3, 3}; + Mat w(4, wShape, CV_32F); + randu(w, -1, 1); + lp.blobs.push_back(w); + Mat b(1, 8, CV_32F, Scalar(0)); + lp.blobs.push_back(b); + net.addLayerToPrev(lp.name, lp.type, lp); + } + + // Softmax + { + LayerParams lp; + lp.type = "Softmax"; + lp.name = "softmax"; + net.addLayerToPrev(lp.name, lp.type, lp); + } + + MatShape inputShape{1, 3, 16, 16}; + Mat input(inputShape, CV_32F); + randu(input, -1, 1); + net.setInput(input); + net.setPreferableBackend(DNN_BACKEND_OPENCV); + + if (!net.getMainGraph()) { + int convId = net.getLayerId("conv1"); + int64 convFlops = net.getFLOPS(convId, inputShape, CV_32F); + int64 expectedConvFlops = (int64)1 * 8 * 16 * 16 * (2 * 3 * 3 * 3 + 1); + EXPECT_EQ(convFlops, expectedConvFlops); + + int softmaxId = net.getLayerId("softmax"); + int64 softmaxFlops = net.getFLOPS(softmaxId, inputShape, CV_32F); + // Softmax output: [1, 8, 16, 16] => 4 * 8 * 16 * 16 + int64 expectedSoftmaxFlops = (int64)4 * 1 * 8 * 16 * 16; + EXPECT_EQ(softmaxFlops, expectedSoftmaxFlops); + } +} + + +TEST(Test_GetFLOPS, MatMulLayer) +{ + // Test MatMul getFLOPS directly via the layer interface + LayerParams lp; + lp.type = "MatMul"; + lp.name = "matmul"; + lp.set("transA", false); + lp.set("transB", false); + + Ptr layer = LayerFactory::createLayerInstance("MatMul", lp); + ASSERT_TRUE(layer); + + // A=[2,4,8], B=[2,8,16] => output=[2,4,16], K=8 + std::vector inputs = {MatShape{2, 4, 8}, MatShape{2, 8, 16}}; + std::vector outputs = {MatShape{2, 4, 16}}; + + int64 flops = layer->getFLOPS(inputs, outputs); + + // batch=2, M=4, N=16, K=8 + // flops = 2 * (2 * 4 * 16 * 8) = 2048 + int64 expected = (int64)2 * (2 * 4 * 16 * 8); + EXPECT_EQ(flops, expected); +} + +TEST(Test_GetFLOPS, MatMulLayerTranspose) +{ + LayerParams lp; + lp.type = "MatMul"; + lp.name = "matmul_t"; + lp.set("transA", true); + lp.set("transB", false); + + Ptr layer = LayerFactory::createLayerInstance("MatMul", lp); + ASSERT_TRUE(layer); + + // transA: A=[2,8,4] => M=4,K=8; B=[2,8,16] => N=16 + std::vector inputs = {MatShape{2, 8, 4}, MatShape{2, 8, 16}}; + std::vector outputs = {MatShape{2, 4, 16}}; + + int64 flops = layer->getFLOPS(inputs, outputs); + int64 expected = (int64)2 * (2 * 4 * 16 * 8); + EXPECT_EQ(flops, expected); +} + +TEST(Test_GetFLOPS, GemmLayer) +{ + LayerParams lp; + lp.type = "Gemm"; + lp.name = "gemm"; + lp.set("transA", false); + lp.set("transB", false); + lp.set("alpha", 1.0f); + lp.set("beta", 1.0f); + lp.set("have_bias", true); + + // B as blob: [128, 64] + Mat B(128, 64, CV_32F); + randu(B, -1, 1); + lp.blobs.push_back(B); + // C as blob: [1, 64] + Mat C(1, 64, CV_32F, Scalar(0)); + lp.blobs.push_back(C); + + Ptr layer = LayerFactory::createLayerInstance("Gemm", lp); + ASSERT_TRUE(layer); + + // A=[32, 128], B=[128, 64] => output=[32, 64] + // M=32, K=128, N=64 + std::vector inputs = {MatShape{32, 128}}; + std::vector outputs = {MatShape{32, 64}}; + + int64 flops = layer->getFLOPS(inputs, outputs); + + // 2*M*N*K + M*N (bias) = 2*32*64*128 + 32*64 = 524,288 + 2,048 = 526,336 + int64 expected = (int64)2 * 32 * 64 * 128 + (int64)32 * 64; + EXPECT_EQ(flops, expected); +} + +TEST(Test_GetFLOPS, AttentionLayer) +{ + LayerParams lp; + lp.type = "Attention"; + lp.name = "attention"; + + int num_heads = 4; + int D = 32; // input hidden size + int hidden = 48; // total projected size (q + k + v) + // qkv_hidden_sizes: q=16, k=16, v=16 + int qkv_sizes[] = {16, 16, 16}; + lp.set("num_heads", num_heads); + lp.set("qkv_hidden_sizes", DictValue::arrayInt(qkv_sizes, 3)); + + // Weight blob: [D, hidden] = [32, 48] + Mat weight(D, hidden, CV_32F); + randu(weight, -1, 1); + lp.blobs.push_back(weight); + // Bias blob: [1, hidden] + Mat bias(1, hidden, CV_32F, Scalar(0)); + lp.blobs.push_back(bias); + + Ptr layer = LayerFactory::createLayerInstance("Attention", lp); + ASSERT_TRUE(layer); + + int64 B = 2, S = 8; + int64 q_size = 16, k_size = 16; + int64 v_size = hidden - q_size - k_size; // 16 + int64 q_head = q_size / num_heads; // 4 + int64 v_head = v_size / num_heads; // 4 + + std::vector inputs = {MatShape{(int)B, (int)S, D}}; + std::vector outputs = {MatShape{(int)B, (int)S, (int)(v_head * num_heads)}}; + + int64 flops = layer->getFLOPS(inputs, outputs); + + // Input projection: B * S * 2 * D * hidden + int64 expected = B * S * (CV_BIG_INT(2) * D * hidden); + // QK^T: B * num_heads * 2 * S * S * q_head + expected += B * num_heads * CV_BIG_INT(2) * S * S * q_head; + // Softmax: B * num_heads * 4 * S * S + expected += B * num_heads * 4 * S * S; + // Attention * V: B * num_heads * 2 * S * v_head * S + expected += B * num_heads * CV_BIG_INT(2) * S * v_head * S; + + EXPECT_EQ(flops, expected); +} + +TEST(Test_GetFLOPS, AttentionOnnxAiLayer) +{ + // Test AttentionOnnxAi (multi-head attention with separate Q, K, V inputs) + LayerParams lp; + lp.type = "AttentionOnnxAi"; + lp.name = "attn_onnxai"; + + int nhq = 4, nhkv = 4; + lp.set("q_num_heads", nhq); + lp.set("kv_num_heads", nhkv); + + Ptr layer = LayerFactory::createLayerInstance("AttentionOnnxAi", lp); + ASSERT_TRUE(layer); + + int64 B = 2, Sq = 8, Skv = 8; + int qk_head = 16, v_head = 16; + + // 4D inputs: [B, num_heads, seq_len, head_dim] + std::vector inputs = { + MatShape{(int)B, nhq, (int)Sq, qk_head}, // Q + MatShape{(int)B, nhkv, (int)Skv, qk_head}, // K + MatShape{(int)B, nhkv, (int)Skv, v_head} // V + }; + std::vector outputs = {MatShape{(int)B, nhq, (int)Sq, v_head}}; + + int64 flops = layer->getFLOPS(inputs, outputs); + + // QK^T: B * nhq * 2 * Sq * Skv * qk_head + int64 expected = B * nhq * CV_BIG_INT(2) * Sq * Skv * qk_head; + // Softmax: B * nhq * 4 * Sq * Skv + expected += B * nhq * 4 * Sq * Skv; + // Attention * V: B * nhq * 2 * Sq * v_head * Skv + expected += B * nhq * CV_BIG_INT(2) * Sq * v_head * Skv; + + EXPECT_EQ(flops, expected); +} + +TEST(Test_GetFLOPS, EinsumLayer) +{ + // Test Einsum: batch matrix multiply "bij,bjk->bik" + LayerParams lp; + lp.type = "Einsum"; + lp.name = "einsum"; + lp.set("equation", "bij,bjk->bik"); + lp.set("inputSize", 2); + lp.set("outputSize", 1); + + Ptr layer = LayerFactory::createLayerInstance("Einsum", lp); + ASSERT_TRUE(layer); + + // A=[2,4,8], B=[2,8,6] => output=[2,4,6] + // Indices: b=2, i=4, j=8, k=6 + std::vector inputs = {MatShape{2, 4, 8}, MatShape{2, 8, 6}}; + std::vector outputs = {MatShape{2, 4, 6}}; + + int64 flops = layer->getFLOPS(inputs, outputs); + + // totalProduct = product of all subscript dims = 2 * 4 * 8 * 6 = 384 + // flops = 2 * totalProduct = 768 + int64 expected = CV_BIG_INT(2) * 2 * 4 * 8 * 6; + EXPECT_EQ(flops, expected); +} + +TEST(Test_GetFLOPS, EinsumLayerTranspose) +{ + // Test Einsum: transpose "ij->ji" + LayerParams lp; + lp.type = "Einsum"; + lp.name = "einsum_transpose"; + lp.set("equation", "ij->ji"); + lp.set("inputSize", 1); + lp.set("outputSize", 1); + + Ptr layer = LayerFactory::createLayerInstance("Einsum", lp); + ASSERT_TRUE(layer); + + std::vector inputs = {MatShape{3, 5}}; + std::vector outputs = {MatShape{5, 3}}; + + int64 flops = layer->getFLOPS(inputs, outputs); + + // Indices: i=3, j=5, totalProduct = 15, flops = 2 * 15 = 30 + int64 expected = CV_BIG_INT(2) * 3 * 5; + EXPECT_EQ(flops, expected); +} + +TEST(Test_GetFLOPS, ZeroFlopsLayers) +{ + // Layers that should return 0 FLOPS (data movement only) + std::vector zeroFlopsTypes = {"Flatten", "Reshape"}; + + for (const auto& typeName : zeroFlopsTypes) { + LayerParams lp; + lp.type = typeName; + lp.name = typeName + "_test"; + if (typeName == "Reshape") { + int newShape[] = {1, -1}; + lp.set("dim", DictValue::arrayInt(newShape, 2)); + } + + Ptr layer = LayerFactory::createLayerInstance(typeName, lp); + if (!layer) continue; + + std::vector inputs = {MatShape{1, 3, 4, 4}}; + std::vector outputs = {MatShape{1, 48}}; + + int64 flops = layer->getFLOPS(inputs, outputs); + EXPECT_EQ(flops, (int64)0) << "Layer type " << typeName << " should have 0 FLOPS"; + } +} + +}} // namespace