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:
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user