1
0
mirror of https://github.com/opencv/opencv.git synced 2026-07-29 07:13:02 +04:00

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
This commit is contained in:
Abhishek Gola
2026-03-13 20:57:14 +05:30
committed by GitHub
parent 1b483ffea6
commit f059d3b517
11 changed files with 786 additions and 12 deletions
+1 -3
View File
@@ -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;
@@ -212,6 +212,34 @@ class AttentionLayerImpl CV_FINAL : public AttentionLayer {
return false;
}
virtual int64 getFLOPS(const std::vector<MatShape> &inputs,
const std::vector<MatShape> &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();
@@ -119,6 +119,27 @@ class AttentionOnnxAiLayerImpl CV_FINAL : public AttentionOnnxAiLayer {
return false;
}
virtual int64 getFLOPS(const std::vector<MatShape> &inputs,
const std::vector<MatShape> &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();
+15
View File
@@ -492,6 +492,21 @@ public:
return true;
} // getMemoryShape
virtual int64 getFLOPS(const std::vector<MatShape> &inputs,
const std::vector<MatShape> &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,
+21
View File
@@ -170,6 +170,27 @@ public:
return false;
}
virtual int64 getFLOPS(const std::vector<MatShape> &inputs,
const std::vector<MatShape> &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<int64>());
// 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) {
+23
View File
@@ -118,6 +118,29 @@ class MatMulLayerImpl CV_FINAL : public MatMulLayer {
return false;
}
virtual int64 getFLOPS(const std::vector<MatShape> &inputs,
const std::vector<MatShape> &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();
@@ -194,6 +194,24 @@ class LSTM2LayerImpl CV_FINAL : public LSTM2Layer
internals.assign(4, inputs[0]);
}
virtual int64 getFLOPS(const std::vector<MatShape> &inputs,
const std::vector<MatShape> &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<int>(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,
+9 -9
View File
@@ -52,21 +52,21 @@ public:
int i, ndims = shapeParam.size();
newShapeDesc.resize(ndims);
for (i = 0; i < ndims; i++) {
int sz = shapeParam.get<int>(i);
if (sz <= 0)
dynamicShapeSpec = true;
newShapeDesc[i] = sz;
newShapeDesc[i] = shapeParam.get<int>(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
+105
View File
@@ -2375,9 +2375,71 @@ std::vector<String> Net::Impl::getUnconnectedOutLayersNames() /*const*/
}
int64 Net::Impl::getFLOPSGraph(const Ptr<Graph>& graph,
const std::vector<MatShape>& shapeCache,
const std::vector<MatType>& typeCache) const
{
if (!graph)
return 0;
int64 flops = 0;
const std::vector<Ptr<Layer>>& prog = graph->prog();
for (const Ptr<Layer>& layer : prog) {
if (!layer)
continue;
const std::vector<Arg>& inputs = layer->inputs;
const std::vector<Arg>& outputs = layer->outputs;
int ninputs = (int)inputs.size();
int noutputs = (int)outputs.size();
std::vector<MatShape> 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<Ptr<Graph>>* subgraphs = layer->subgraphs();
if (subgraphs) {
for (const Ptr<Graph>& sg : *subgraphs)
flops += getFLOPSGraph(sg, shapeCache, typeCache);
}
}
return flops;
}
int64 Net::Impl::getFLOPS(const std::vector<MatShape>& netInputShapes,
const std::vector<MatType>& netInputTypes) /*const*/
{
if (mainGraph) {
LayerShapes shapes;
std::vector<MatShape> shapeCache;
std::vector<MatType> typeCache;
tryInferShapes(netInputShapes, netInputTypes, shapes, shapeCache, typeCache);
return getFLOPSGraph(mainGraph, shapeCache, typeCache);
}
int64 flops = 0;
std::vector<int> ids;
std::vector<std::vector<MatShape>> inShapes, outShapes;
@@ -2399,6 +2461,49 @@ int64 Net::Impl::getFLOPS(
const std::vector<MatShape>& netInputShapes,
const std::vector<MatType>& netInputTypes) /*const*/
{
if (mainGraph) {
LayerShapes shapes;
std::vector<MatShape> shapeCache;
std::vector<MatType> typeCache;
tryInferShapes(netInputShapes, netInputTypes, shapes, shapeCache, typeCache);
CV_Assert(0 <= layerId && layerId < (int)totalLayers);
int localIdx = layerId;
for (const Ptr<Graph>& graph : allgraphs) {
int progSize = (int)graph->prog().size();
if (localIdx < progSize) {
const Ptr<Layer>& layer = graph->prog()[localIdx];
if (!layer)
return 0;
const std::vector<Arg>& inputs = layer->inputs;
const std::vector<Arg>& outputs = layer->outputs;
int ninputs = (int)inputs.size();
int noutputs = (int)outputs.size();
std::vector<MatShape> 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());
+3
View File
@@ -305,6 +305,9 @@ struct Net::Impl : public detail::NetImplBase
const int layerId,
const std::vector<MatShape>& netInputShapes,
const std::vector<MatType>& netInputTypes) /*const*/;
int64 getFLOPSGraph(const Ptr<Graph>& graph,
const std::vector<MatShape>& shapeCache,
const std::vector<MatType>& typeCache) const;
void getMemoryConsumption(
const int layerId,
+542
View File
@@ -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 <opencv2/dnn/all_layers.hpp>
#include <opencv2/dnn/shape_utils.hpp>
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> layer = LayerFactory::createLayerInstance("MatMul", lp);
ASSERT_TRUE(layer);
// A=[2,4,8], B=[2,8,16] => output=[2,4,16], K=8
std::vector<MatShape> inputs = {MatShape{2, 4, 8}, MatShape{2, 8, 16}};
std::vector<MatShape> 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> 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<MatShape> inputs = {MatShape{2, 8, 4}, MatShape{2, 8, 16}};
std::vector<MatShape> 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> 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<MatShape> inputs = {MatShape{32, 128}};
std::vector<MatShape> 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> 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<MatShape> inputs = {MatShape{(int)B, (int)S, D}};
std::vector<MatShape> 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> 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<MatShape> 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<MatShape> 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> 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<MatShape> inputs = {MatShape{2, 4, 8}, MatShape{2, 8, 6}};
std::vector<MatShape> 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> layer = LayerFactory::createLayerInstance("Einsum", lp);
ASSERT_TRUE(layer);
std::vector<MatShape> inputs = {MatShape{3, 5}};
std::vector<MatShape> 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<std::string> 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> layer = LayerFactory::createLayerInstance(typeName, lp);
if (!layer) continue;
std::vector<MatShape> inputs = {MatShape{1, 3, 4, 4}};
std::vector<MatShape> outputs = {MatShape{1, 48}};
int64 flops = layer->getFLOPS(inputs, outputs);
EXPECT_EQ(flops, (int64)0) << "Layer type " << typeName << " should have 0 FLOPS";
}
}
}} // namespace