mirror of
https://github.com/opencv/opencv.git
synced 2026-09-11 04:43:22 -05:00
Merge pull request #29594 from abhishek-gola:bitcast_matmul_dft_layers
Added Bitcast layer & extended MatMul and DFT layers support - #29594 ### 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:
@@ -375,6 +375,8 @@ CV__DNN_INLINE_NS_BEGIN
|
||||
virtual bool fuseBatchNorm(const Ptr<Layer>& bn) = 0;
|
||||
virtual bool fuseActivation(const Ptr<Layer>& activ) = 0;
|
||||
virtual bool fuseAddResidual(Arg residual) = 0;
|
||||
// Folds a trailing scalar multiply into the pre-activation scale/bias; requires scale >= 0 and act(x)*s == act(x*s).
|
||||
virtual bool fuseTrailingScale(InputArray scale) = 0;
|
||||
|
||||
std::vector<int> strides, dilations, pads;
|
||||
int ngroups;
|
||||
@@ -1974,10 +1976,15 @@ CV__DNN_INLINE_NS_BEGIN
|
||||
};
|
||||
|
||||
class CV_EXPORTS CumProdLayer : public Layer {
|
||||
public:
|
||||
public:
|
||||
static Ptr<CumProdLayer> create(const LayerParams ¶ms);
|
||||
};
|
||||
|
||||
class CV_EXPORTS BitCastLayer : public Layer {
|
||||
public:
|
||||
static Ptr<BitCastLayer> create(const LayerParams ¶ms);
|
||||
};
|
||||
|
||||
class CV_EXPORTS LinearAttentionLayer : public Layer {
|
||||
public:
|
||||
static Ptr<LinearAttentionLayer> create(const LayerParams ¶ms);
|
||||
|
||||
@@ -131,6 +131,24 @@ struct ModelFusionBasic
|
||||
}
|
||||
}
|
||||
|
||||
// fold a trailing scalar multiply into 'conv' (e.g. exported "Conv -> ReLU -> Mul(scale)"); safety is checked inside fuseTrailingScale().
|
||||
if (elemwise && elemwise->op == NaryEltwiseLayer::OPERATION::PROD &&
|
||||
ninputs == 2) {
|
||||
int const_idx = netimpl->isConstArg(inputs[0]) ? 0 :
|
||||
netimpl->isConstArg(inputs[1]) ? 1 : -1;
|
||||
if (const_idx >= 0) {
|
||||
Arg conv_out = inputs[1 - const_idx];
|
||||
int conv_layer_idx = producer_of.at(conv_out.idx);
|
||||
Conv2Layer* conv = getLayer<Conv2Layer>(newprog, conv_layer_idx);
|
||||
if (conv && usecounts.at(conv_out.idx) == 1 &&
|
||||
conv->fuseTrailingScale(netimpl->argTensor(inputs[const_idx]))) {
|
||||
fused_layer_idx = conv_layer_idx;
|
||||
removed_args.push_back(conv_out);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fuse Reshape + InstanceNorm(scale=ones,bias=zeros) + Reshape + Mul + Add
|
||||
if (elemwise && elemwise->op == NaryEltwiseLayer::OPERATION::ADD &&
|
||||
ninputs == 2) {
|
||||
|
||||
@@ -236,6 +236,7 @@ void initializeLayerFactory()
|
||||
CV_DNN_REGISTER_LAYER_CLASS(SDPA, SDPALayer);
|
||||
CV_DNN_REGISTER_LAYER_CLASS(AttentionOnnxAi, AttentionOnnxAiLayer);
|
||||
CV_DNN_REGISTER_LAYER_CLASS(CausalConvWithState, CausalConvWithStateLayer);
|
||||
CV_DNN_REGISTER_LAYER_CLASS(BitCast, BitCastLayer);
|
||||
CV_DNN_REGISTER_LAYER_CLASS(LinearAttention, LinearAttentionLayer);
|
||||
CV_DNN_REGISTER_LAYER_CLASS(FlexAttention, FlexAttentionLayer);
|
||||
CV_DNN_REGISTER_LAYER_CLASS(RotaryEmbedding, RotaryEmbeddingLayer);
|
||||
|
||||
81
modules/dnn/src/layers/bitcast_layer.cpp
Normal file
81
modules/dnn/src/layers/bitcast_layer.cpp
Normal file
@@ -0,0 +1,81 @@
|
||||
// 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) 2026, BigVision LLC, all rights reserved.
|
||||
// Third party copyrights are property of their respective owners.
|
||||
|
||||
#include "../precomp.hpp"
|
||||
#include "layers_common.hpp"
|
||||
|
||||
namespace cv {
|
||||
namespace dnn {
|
||||
|
||||
/*
|
||||
Implementation of BitCast, as defined in ONNX specification:
|
||||
https://onnx.ai/onnx/operators/onnx__BitCast.html
|
||||
|
||||
Opset 26 is covered.
|
||||
*/
|
||||
class BitCastLayerImpl CV_FINAL : public BitCastLayer
|
||||
{
|
||||
public:
|
||||
BitCastLayerImpl(const LayerParams& params)
|
||||
{
|
||||
setParamsFrom(params);
|
||||
outputType = params.get<int>("outputType");
|
||||
}
|
||||
|
||||
bool supportBackend(int backendId) CV_OVERRIDE
|
||||
{
|
||||
return backendId == DNN_BACKEND_OPENCV;
|
||||
}
|
||||
|
||||
bool getMemoryShapes(const std::vector<MatShape>& inputs,
|
||||
const int /*requiredOutputs*/,
|
||||
std::vector<MatShape>& outputs,
|
||||
std::vector<MatShape>& /*internals*/) const CV_OVERRIDE
|
||||
{
|
||||
CV_CheckEQ(inputs.size(), (size_t)1, "BitCast takes exactly one input");
|
||||
outputs.assign(1, inputs[0]);
|
||||
return false;
|
||||
}
|
||||
|
||||
void getTypes(const std::vector<MatType>& inputs,
|
||||
const int requiredOutputs,
|
||||
const int /*requiredInternals*/,
|
||||
std::vector<MatType>& outputs,
|
||||
std::vector<MatType>& internals) const CV_OVERRIDE
|
||||
{
|
||||
CV_CheckEQ(inputs.size(), (size_t)1, "");
|
||||
CV_CheckEQ(CV_ELEM_SIZE1(inputs[0]), CV_ELEM_SIZE1(outputType),
|
||||
"BitCast: input and target types must have equal bit-width (ONNX spec)");
|
||||
outputs.assign(requiredOutputs, MatType(outputType));
|
||||
internals.clear();
|
||||
}
|
||||
|
||||
void forward(InputArrayOfArrays inputs_arr, OutputArrayOfArrays outputs_arr, OutputArrayOfArrays) CV_OVERRIDE
|
||||
{
|
||||
std::vector<Mat> inputs, outputs;
|
||||
inputs_arr.getMatVector(inputs);
|
||||
outputs_arr.getMatVector(outputs);
|
||||
|
||||
const Mat& src = inputs[0];
|
||||
Mat& dst = outputs[0];
|
||||
CV_CheckEQ(src.elemSize(), dst.elemSize(), "BitCast: element sizes must match");
|
||||
CV_CheckEQ(src.total(), dst.total(), "");
|
||||
|
||||
// Reinterprets dst's buffer as src's layout so copyTo strided-copies non-contiguous src.
|
||||
Mat dstAsSrcLayout(src.shape(), src.type(), dst.data);
|
||||
src.copyTo(dstAsSrcLayout);
|
||||
}
|
||||
|
||||
private:
|
||||
int outputType;
|
||||
};
|
||||
|
||||
Ptr<BitCastLayer> BitCastLayer::create(const LayerParams& params)
|
||||
{
|
||||
return makePtr<BitCastLayerImpl>(params);
|
||||
}
|
||||
|
||||
}} // namespace cv::dnn
|
||||
@@ -286,6 +286,51 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool fuseTrailingScale(InputArray scale_arr) CV_OVERRIDE
|
||||
{
|
||||
// act(x)*s == act(x*s) only for s >= 0 and a positively homogeneous act; PReLU's slope and Clip's bounds break that.
|
||||
if (activationFunc != nullptr || !activ.empty() || addResidual ||
|
||||
(fastActivation != FAST_ACTIV_NONE && fastActivation != FAST_ACTIV_RELU &&
|
||||
fastActivation != FAST_ACTIV_LEAKY_RELU))
|
||||
return false;
|
||||
if (wshape0.empty())
|
||||
return false;
|
||||
|
||||
// Scalar only: a per-channel scale needs the same broadcast-axis check fuseBatchNormWeights() does.
|
||||
Mat s = scale_arr.getMat();
|
||||
if (s.empty() || s.total() != 1)
|
||||
return false;
|
||||
int stype = s.type();
|
||||
if (stype != CV_32F && stype != CV_64F)
|
||||
return false;
|
||||
|
||||
const float scalar = stype == CV_32F ? s.ptr<float>()[0] : (float)s.ptr<double>()[0];
|
||||
if (!(scalar >= 0.f)) // also rejects NaN
|
||||
return false;
|
||||
|
||||
const int K = wshape0[0];
|
||||
if (!fusedBatchNorm) {
|
||||
const float* bp = bias.empty() ? nullptr : bias.ptr<float>();
|
||||
fusedScale.fit(1, &K, CV_32F);
|
||||
fusedBias.fit(1, &K, CV_32F);
|
||||
float* fs = fusedScale.ptr<float>();
|
||||
float* fb = fusedBias.ptr<float>();
|
||||
for (int k = 0; k < K; k++) {
|
||||
fs[k] = scalar;
|
||||
fb[k] = (bp ? bp[k] : 0.f) * scalar;
|
||||
}
|
||||
fusedBatchNorm = true;
|
||||
} else {
|
||||
float* fs = fusedScale.ptr<float>();
|
||||
float* fb = fusedBias.ptr<float>();
|
||||
for (int k = 0; k < K; k++) {
|
||||
fs[k] *= scalar;
|
||||
fb[k] *= scalar;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual int64_t getFLOPS(const std::vector<MatShape>& inputs,
|
||||
const std::vector<MatShape>& outputs) const CV_OVERRIDE
|
||||
{
|
||||
|
||||
@@ -131,6 +131,82 @@ static void runTypedDFT(const Mat& src,
|
||||
}
|
||||
}
|
||||
|
||||
// Offset of the pos-th signal, walking the dims that are not transformed.
|
||||
static inline size_t dftSignalOffset(int pos,
|
||||
const std::vector<int>& iterDims,
|
||||
const std::vector<int>& outerSizes,
|
||||
const std::vector<size_t>& outerStep,
|
||||
const std::vector<size_t>& strides)
|
||||
{
|
||||
size_t base = 0;
|
||||
for (size_t t = 0; t < iterDims.size(); ++t)
|
||||
{
|
||||
int idxVal = outerStep.empty() ? 0 : (int)((pos / outerStep[t]) % (size_t)outerSizes[t]);
|
||||
base += (size_t)idxVal * strides[iterDims[t]];
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
// Inverse real DFT: cv::dft() takes the one-sided spectrum in CCS layout directly.
|
||||
template<typename T>
|
||||
static void runIRFFT(const Mat& src, Mat& dst,
|
||||
const std::vector<size_t>& stridesSrc,
|
||||
const std::vector<size_t>& stridesDst,
|
||||
const std::vector<int>& iterDims,
|
||||
const std::vector<int>& outerSizes,
|
||||
const std::vector<size_t>& outerStep,
|
||||
const size_t totalOuter,
|
||||
const int N, const int L,
|
||||
const size_t strideAxisSrc, const size_t strideAxisDst)
|
||||
{
|
||||
const int matTypeReal = std::is_same<T, float>::value ? CV_32F : CV_64F;
|
||||
const T* sp = src.ptr<T>();
|
||||
T* dp = dst.ptr<T>();
|
||||
|
||||
// Innermost axis: dst rows are already the N-element rows cv::dft() wants.
|
||||
const bool inPlaceOnDst = (strideAxisDst == 1);
|
||||
Mat rows = inPlaceOnDst ? Mat((int)totalOuter, N, matTypeReal, dp)
|
||||
: Mat((int)totalOuter, N, matTypeReal);
|
||||
|
||||
const int nbins = std::min(L, N / 2 + 1); // bins a CCS row can hold
|
||||
const bool zeroPad = nbins < N / 2 + 1;
|
||||
|
||||
// One DFT_ROWS call per stripe: cv::dft() rebuilds its tables on every call.
|
||||
const double nstripes = std::min<double>((double)totalOuter, std::max(getNumThreads(), 1));
|
||||
parallel_for_(Range(0, (int)totalOuter), [&](const Range& r)
|
||||
{
|
||||
for (int pos = r.start; pos < r.end; ++pos)
|
||||
{
|
||||
const T* in = sp + dftSignalOffset(pos, iterDims, outerSizes, outerStep, stridesSrc);
|
||||
T* ccs = rows.ptr<T>(pos);
|
||||
if (zeroPad)
|
||||
std::fill(ccs, ccs + N, T(0));
|
||||
// CCS: [Re0, Re1, Im1, ...] plus a trailing Re(N/2) for even N.
|
||||
ccs[0] = in[0];
|
||||
for (int k = 1; k < nbins; ++k)
|
||||
{
|
||||
const size_t o = (size_t)k * strideAxisSrc;
|
||||
ccs[2 * k - 1] = in[o];
|
||||
if (2 * k < N)
|
||||
ccs[2 * k] = in[o + 1];
|
||||
}
|
||||
}
|
||||
|
||||
Mat stripe = rows.rowRange(r.start, r.end);
|
||||
cv::dft(stripe, stripe, DFT_INVERSE | DFT_SCALE | DFT_ROWS);
|
||||
|
||||
if (inPlaceOnDst)
|
||||
return; // dst already holds the result
|
||||
for (int pos = r.start; pos < r.end; ++pos)
|
||||
{
|
||||
const T* res = rows.ptr<T>(pos);
|
||||
T* out = dp + dftSignalOffset(pos, iterDims, outerSizes, outerStep, stridesDst);
|
||||
for (int n = 0; n < N; ++n)
|
||||
out[(size_t)n * strideAxisDst] = res[n];
|
||||
}
|
||||
}, nstripes);
|
||||
}
|
||||
|
||||
class DFTLayerImpl CV_FINAL : public DFTLayer {
|
||||
public:
|
||||
DFTLayerImpl(const LayerParams ¶ms)
|
||||
@@ -143,13 +219,14 @@ public:
|
||||
|
||||
virtual bool dynamicOutputShapes() const CV_OVERRIDE
|
||||
{
|
||||
if (this->inputs.size() >= 2)
|
||||
// Non-const dft_length/axis inputs make the output shape known only at forward time.
|
||||
Net::Impl* netimpl_ = getNetImpl(const_cast<DFTLayerImpl*>(this));
|
||||
for (size_t i = 1; i < this->inputs.size(); ++i)
|
||||
{
|
||||
Net::Impl* netimpl_ = getNetImpl(const_cast<DFTLayerImpl*>(this));
|
||||
if (!netimpl_ || !netimpl_->isConstArg(this->inputs[1]))
|
||||
{
|
||||
if (this->inputs[i].idx <= 0)
|
||||
continue; // empty optional input
|
||||
if (!netimpl_ || !netimpl_->isConstArg(this->inputs[i]))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -183,6 +260,31 @@ private:
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read the opset-20 axis input when it is a constant.
|
||||
int getAxisFromConstant() const
|
||||
{
|
||||
if (this->inputs.size() < 3 || this->inputs[2].idx <= 0)
|
||||
{
|
||||
return INT_MIN; // no axis input, or empty optional input
|
||||
}
|
||||
|
||||
Net::Impl* netimpl_ = getNetImpl(const_cast<DFTLayerImpl*>(this));
|
||||
if (!netimpl_ || !netimpl_->isConstArg(this->inputs[2]))
|
||||
{
|
||||
return INT_MIN;
|
||||
}
|
||||
|
||||
Mat axis_tensor = netimpl_->argTensor(this->inputs[2]);
|
||||
if (axis_tensor.empty() || axis_tensor.total() != 1)
|
||||
{
|
||||
return INT_MIN;
|
||||
}
|
||||
|
||||
int64_t axis64 = 0;
|
||||
tensorToScalar(axis_tensor, CV_64S, &axis64);
|
||||
return static_cast<int>(axis64);
|
||||
}
|
||||
|
||||
public:
|
||||
virtual bool getMemoryShapes(const std::vector<MatShape> &inputs,
|
||||
const int /*requiredOutputs*/,
|
||||
@@ -194,12 +296,6 @@ public:
|
||||
CV_Assert(!inshape.empty());
|
||||
MatShape out = inshape;
|
||||
|
||||
int last = out.back();
|
||||
if (last == 1)
|
||||
out.back() = 2;
|
||||
else if (last != 2)
|
||||
out.push_back(2);
|
||||
|
||||
int ndims_in = (int)inshape.size();
|
||||
int ax = axis_attr;
|
||||
if (ax == INT_MIN)
|
||||
@@ -207,6 +303,33 @@ public:
|
||||
ax = (inshape.back() == 2 || inshape.back() == 1) ? ndims_in - 2 : ndims_in - 1;
|
||||
}
|
||||
if (ax < 0) ax += ndims_in;
|
||||
|
||||
// A constant opset-20 axis input overrides the attribute (matches forward()).
|
||||
int ax_const = getAxisFromConstant();
|
||||
if (ax_const != INT_MIN)
|
||||
{
|
||||
ax = ax_const < 0 ? ax_const + ndims_in : ax_const;
|
||||
}
|
||||
|
||||
// Inverse real DFT (irfft): real output, axis restored to full length N.
|
||||
if (inverse && onesided)
|
||||
{
|
||||
out.back() = 1;
|
||||
if (ax >= 0 && ax < (int)out.size() - 1)
|
||||
{
|
||||
int dft_length = getDftLengthFromConstant();
|
||||
out[ax] = dft_length > 0 ? dft_length : 2 * (inshape[ax] - 1);
|
||||
}
|
||||
outputs.assign(1, out);
|
||||
return false;
|
||||
}
|
||||
|
||||
int last = out.back();
|
||||
if (last == 1)
|
||||
out.back() = 2;
|
||||
else if (last != 2)
|
||||
out.push_back(2);
|
||||
|
||||
if (ax >= 0 && ax < (int)out.size() - 1)
|
||||
{
|
||||
int dft_length = getDftLengthFromConstant();
|
||||
@@ -243,12 +366,25 @@ public:
|
||||
axis = axes[0];
|
||||
if (axis < 0) axis += ndims;
|
||||
}
|
||||
// opset-20 provides the axis as the 3rd input tensor.
|
||||
if (inputs.size() >= 3 && !inputs[2].empty())
|
||||
{
|
||||
CV_Assert(inputs[2].total() == 1);
|
||||
int64_t ax64 = 0;
|
||||
tensorToScalar(inputs[2], CV_64S, &ax64);
|
||||
axis = static_cast<int>(ax64);
|
||||
if (axis < 0) axis += ndims;
|
||||
}
|
||||
CV_Assert(axis >= 0 && axis < (srcHasComplex ? ndims - 1 : ndims));
|
||||
if (onesided)
|
||||
|
||||
const bool irfft = inverse && onesided;
|
||||
if (onesided && !irfft)
|
||||
{
|
||||
CV_Assert(!srcHasComplex);
|
||||
CV_Assert(!inverse);
|
||||
}
|
||||
if (irfft)
|
||||
CV_Assert(srcHasComplex);
|
||||
|
||||
int dft_length = -1;
|
||||
if (inputs.size() >= 2 && !inputs[1].empty())
|
||||
@@ -259,6 +395,48 @@ public:
|
||||
dft_length = static_cast<int>(dft_length64);
|
||||
}
|
||||
|
||||
if (irfft)
|
||||
{
|
||||
const int L = src.size[axis];
|
||||
const int N = dft_length > 0 ? dft_length : 2 * (L - 1);
|
||||
|
||||
std::vector<int> outSizesVec(ndims);
|
||||
for (int i = 0; i < ndims; ++i) outSizesVec[i] = src.size[i];
|
||||
outSizesVec[ndims - 1] = 1; // real output
|
||||
outSizesVec[axis] = N;
|
||||
|
||||
MatShape outShape(outSizesVec.begin(), outSizesVec.end());
|
||||
if (outputs_arr.kind() == _InputArray::STD_VECTOR_MAT)
|
||||
outputs_arr.getMatVecRef()[0].fit(outShape, src.type());
|
||||
else
|
||||
outputs_arr.getUMatVecRef()[0].fit(outShape, src.type());
|
||||
outputs_arr.getMatVector(outputs);
|
||||
Mat &dst = outputs[0];
|
||||
|
||||
std::vector<size_t> stridesSrc(ndims, 1), stridesDst(ndims, 1);
|
||||
for (int i = ndims - 2; i >= 0; --i) stridesSrc[i] = stridesSrc[i + 1] * (size_t)src.size[i + 1];
|
||||
for (int i = ndims - 2; i >= 0; --i) stridesDst[i] = stridesDst[i + 1] * (size_t)outSizesVec[i + 1];
|
||||
|
||||
std::vector<int> iterDims;
|
||||
for (int i = 0; i < ndims - 1; ++i) if (i != axis) iterDims.push_back(i);
|
||||
std::vector<int> outerSizes(iterDims.size());
|
||||
for (size_t j = 0; j < iterDims.size(); ++j) outerSizes[j] = src.size[iterDims[j]];
|
||||
std::vector<size_t> outerStep(iterDims.size(), 1);
|
||||
for (int j = (int)iterDims.size() - 2; j >= 0; --j) outerStep[j] = outerStep[j + 1] * (size_t)outerSizes[j + 1];
|
||||
size_t totalOuter = 1;
|
||||
for (int s : outerSizes) totalOuter *= (size_t)s;
|
||||
|
||||
const size_t strideAxisSrc = stridesSrc[axis];
|
||||
const size_t strideAxisDst = stridesDst[axis];
|
||||
if (src.depth() == CV_32F)
|
||||
runIRFFT<float>(src, dst, stridesSrc, stridesDst, iterDims, outerSizes, outerStep, totalOuter, N, L, strideAxisSrc, strideAxisDst);
|
||||
else if (src.depth() == CV_64F)
|
||||
runIRFFT<double>(src, dst, stridesSrc, stridesDst, iterDims, outerSizes, outerStep, totalOuter, N, L, strideAxisSrc, strideAxisDst);
|
||||
else
|
||||
CV_Error(Error::StsNotImplemented, "DFT supports float32/float64 only");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<int> outSizesVec;
|
||||
outSizesVec.resize(srcHasComplex ? ndims : ndims + (srcLastIsOne ? 0 : 1));
|
||||
for (int i = 0; i < ndims; ++i) outSizesVec[i] = src.size[i];
|
||||
|
||||
@@ -60,6 +60,54 @@ class MatMulLayerImpl CV_FINAL : public MatMulLayer {
|
||||
backendId == DNN_BACKEND_CANN;
|
||||
}
|
||||
|
||||
// numpy 1-D promotion: A[K]->[1,K], B[K]->[K,1]. `out` drops the inserted 1-dims.
|
||||
static void matmulShapes(const MatShape& rawA, const MatShape& rawB, bool trans_a, bool trans_b,
|
||||
MatShape& Ap, MatShape& Bp, MatShape& full, MatShape& out) {
|
||||
const bool a1d = rawA.size() == 1, b1d = rawB.size() == 1;
|
||||
Ap = a1d ? MatShape{1, rawA[0]} : rawA;
|
||||
Bp = b1d ? MatShape{rawB[0], 1} : rawB;
|
||||
CV_CheckGE(Ap.size(), (size_t)2, "DNN/MatMul: invalid shape of input A");
|
||||
CV_CheckGE(Bp.size(), (size_t)2, "DNN/MatMul: invalid shape of input B");
|
||||
|
||||
int mA = Ap[Ap.size() - 2], nA = Ap.back();
|
||||
int mB = Bp[Bp.size() - 2], nB = Bp.back();
|
||||
int M = trans_a ? nA : mA;
|
||||
int N = trans_b ? mB : nB;
|
||||
int K_A = trans_a ? mA : nA;
|
||||
int K_B = trans_b ? nB : mB;
|
||||
CV_CheckEQ(K_A, K_B, "DNN/MatMul: invalid dimension K");
|
||||
|
||||
if (Ap.size() != 2 || Bp.size() != 2) {
|
||||
const auto &more = Ap.size() > Bp.size() ? Ap : Bp;
|
||||
const auto &less = Ap.size() > Bp.size() ? Bp : Ap;
|
||||
size_t diff_dims = more.size() - less.size();
|
||||
full = more;
|
||||
for (size_t i = 0; i < less.size() - 2; i++) {
|
||||
const auto dl = less[i], dm = more[i + diff_dims];
|
||||
if (dl != 1 && dm != 1 && dl != dm)
|
||||
CV_Error(Error::StsBadSize, "DNN/MatMul: invalid shape for broadcasting");
|
||||
if (dm == 1)
|
||||
full[i + diff_dims] = dl;
|
||||
}
|
||||
full[full.size() - 2] = M;
|
||||
full[full.size() - 1] = N;
|
||||
} else {
|
||||
full = MatShape{M, N};
|
||||
}
|
||||
|
||||
// both 1-D, no batch -> 0-D scalar (total 1), not empty (total 0)
|
||||
if (a1d && b1d && full.size() == 2) {
|
||||
out = MatShape::scalar();
|
||||
return;
|
||||
}
|
||||
// drop the 1-dims inserted by promotion
|
||||
out.clear();
|
||||
for (size_t i = 0; i + 2 < full.size(); i++)
|
||||
out.push_back(full[i]);
|
||||
if (!a1d) out.push_back(M);
|
||||
if (!b1d) out.push_back(N);
|
||||
}
|
||||
|
||||
virtual bool getMemoryShapes(const std::vector<MatShape> &inputs,
|
||||
const int requiredOutputs,
|
||||
std::vector<MatShape> &outputs,
|
||||
@@ -69,42 +117,12 @@ class MatMulLayerImpl CV_FINAL : public MatMulLayer {
|
||||
CV_CheckLE(num_inputs, 3, "DNN/MatMul: three inputs at most");
|
||||
|
||||
const auto shape_A = inputs[0], shape_B = blobs.empty() ? inputs[1] : shape(blobs[0]);
|
||||
CV_CheckGE(shape_A.size(), static_cast<size_t>(2), "DNN/MatMul: invalid shape of input A");
|
||||
CV_CheckGE(shape_B.size(), static_cast<size_t>(2), "DNN/MatMul: invalid shape of input B");
|
||||
CV_CheckGE(shape_A.size(), static_cast<size_t>(1), "DNN/MatMul: invalid shape of input A");
|
||||
CV_CheckGE(shape_B.size(), static_cast<size_t>(1), "DNN/MatMul: invalid shape of input B");
|
||||
|
||||
// Check legal matrix multiplication
|
||||
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_A = trans_a ? mA : nA;
|
||||
int K_B = trans_b ? nB : mB;
|
||||
CV_CheckEQ(K_A, K_B, "DNN/MatMul: invalid dimension K");
|
||||
|
||||
// Check if inputs are broadcastable.
|
||||
MatShape common_shape;
|
||||
if (shape_A.size() != 2 || shape_B.size() != 2) {
|
||||
const auto &shape_more_dims = shape_A.size() > shape_B.size() ? shape_A : shape_B;
|
||||
const auto &shape_less_dims = shape_A.size() > shape_B.size() ? shape_B : shape_A;
|
||||
size_t diff_dims = shape_more_dims.size() - shape_less_dims.size();
|
||||
common_shape = shape_more_dims;
|
||||
for (size_t i = 0; i < shape_less_dims.size() - 2; i++) {
|
||||
const auto dl = shape_less_dims[i], dm = shape_more_dims[i + diff_dims];
|
||||
if (dl != 1 && dm != 1 && dl != dm) {
|
||||
CV_Error(Error::StsBadSize, format("DNN/MatMul: invalid shape for broadcasting, shape_A[%zu]=%d, shape_B[%zu]=%d\n", i, shape_less_dims[i], i, shape_more_dims[i + diff_dims]));
|
||||
}
|
||||
|
||||
if (dm == 1) {
|
||||
common_shape[i + diff_dims] = dl;
|
||||
}
|
||||
}
|
||||
common_shape[common_shape.size() - 2] = M;
|
||||
common_shape[common_shape.size() - 1] = N;
|
||||
} else {
|
||||
common_shape.resize(2);
|
||||
common_shape[0] = M;
|
||||
common_shape[1] = N;
|
||||
}
|
||||
MatShape shape_Ap, shape_Bp, common_shape, out_shape;
|
||||
matmulShapes(shape_A, shape_B, trans_a, trans_b, shape_Ap, shape_Bp, common_shape, out_shape);
|
||||
int N = common_shape.back();
|
||||
|
||||
// Check if bias is broadcastable
|
||||
if (num_inputs == 3) {
|
||||
@@ -124,7 +142,7 @@ class MatMulLayerImpl CV_FINAL : public MatMulLayer {
|
||||
}
|
||||
}
|
||||
|
||||
outputs.assign(1, common_shape);
|
||||
outputs.assign(1, out_shape);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -148,16 +166,18 @@ class MatMulLayerImpl CV_FINAL : public MatMulLayer {
|
||||
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();
|
||||
// Promote 1-D operands so shape.size()-2 can't underflow on a 1-D input.
|
||||
MatShape shape_Ap, shape_Bp, full_shape, out_shape;
|
||||
matmulShapes(inputs[0], blobs.empty() ? inputs[1] : shape(blobs[0]),
|
||||
trans_a, trans_b, shape_Ap, shape_Bp, full_shape, out_shape);
|
||||
int mA = shape_Ap[shape_Ap.size() - 2], nA = shape_Ap.back();
|
||||
int M = trans_a ? nA : mA;
|
||||
int N = trans_b ? mB : nB;
|
||||
int K = trans_a ? mA : nA;
|
||||
int N = full_shape.back();
|
||||
|
||||
int64 batch = 1;
|
||||
for (size_t i = 0; i + 2 < outputs[0].size(); i++)
|
||||
batch *= outputs[0][i];
|
||||
for (size_t i = 0; i + 2 < full_shape.size(); i++)
|
||||
batch *= full_shape[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);
|
||||
@@ -174,9 +194,9 @@ class MatMulLayerImpl CV_FINAL : public MatMulLayer {
|
||||
inputs_arr.getMatVector(inputs);
|
||||
outputs_arr.getMatVector(outputs);
|
||||
|
||||
const auto A_shape = shape(inputs[0]),
|
||||
B_shape = blobs.empty() ? shape(inputs[1]) : shape(blobs[0]),
|
||||
C_shape = shape(outputs[0]);
|
||||
MatShape A_shape, B_shape, C_shape, out_shape;
|
||||
matmulShapes(shape(inputs[0]), blobs.empty() ? shape(inputs[1]) : shape(blobs[0]),
|
||||
trans_a, trans_b, A_shape, B_shape, C_shape, out_shape);
|
||||
helper.compute(trans_a, trans_b, A_shape, B_shape, C_shape);
|
||||
|
||||
// These five types skip the float-only packed-B/MLAS caching below.
|
||||
@@ -189,6 +209,17 @@ class MatMulLayerImpl CV_FINAL : public MatMulLayer {
|
||||
// Pack only 2D weight matrices; skip higher-dim tensors (e.g. Q@K^T in attention).
|
||||
const Mat* B_mat = !blobs.empty() ? &blobs[0] :
|
||||
(inputs.size() >= 2 && inputs[1].dims == 2 ? &inputs[1] : nullptr);
|
||||
|
||||
// A constant rank-1 weight ([K] in the logical [M, K] @ [K] -> [M] contract) is
|
||||
// still physically 1-D here; fastGemmPackB/fastGemmThinPackB read the last two
|
||||
// dims, so promote it to [K, 1] first. reshape() shares the blob's buffer, so
|
||||
// last_packed_input_B_data below still tracks the original data pointer.
|
||||
Mat promoted_B;
|
||||
if (B_mat && B_mat->dims == 1) {
|
||||
promoted_B = B_mat->reshape(1, std::vector<int>{B_mat->size[0], 1});
|
||||
B_mat = &promoted_B;
|
||||
}
|
||||
|
||||
if (B_mat && B_mat->data != last_packed_input_B_data) {
|
||||
packed_input_B.clear();
|
||||
packed_input_B.shrink_to_fit();
|
||||
@@ -274,7 +305,13 @@ class MatMulLayerImpl CV_FINAL : public MatMulLayer {
|
||||
default: break;
|
||||
}
|
||||
|
||||
const auto &A = inputs[0];
|
||||
// Promote 1-D operands (numpy MatMul semantics) so leading dims match helper.
|
||||
Mat A = inputs[0];
|
||||
{ MatShape sa = shape(A); if (sa.size() == 1) A = A.reshape(1, std::vector<int>{1, sa[0]}); }
|
||||
if (blobs.empty()) {
|
||||
MatShape sb = shape(inputs[1]);
|
||||
if (sb.size() == 1) inputs[1] = inputs[1].reshape(1, std::vector<int>{sb[0], 1});
|
||||
}
|
||||
auto &Y = outputs[0];
|
||||
|
||||
const auto *a = A.ptr<const float>();
|
||||
|
||||
@@ -190,6 +190,7 @@ protected:
|
||||
void parseBatchNormalization (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
|
||||
void parseCast (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
|
||||
void parseCast2 (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
|
||||
void parseBitCast (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
|
||||
void parseCastLike (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
|
||||
void parseClip (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
|
||||
void parseConcat (LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto);
|
||||
@@ -1729,6 +1730,16 @@ void ONNXImporter2::parseCastLike(LayerParams& layerParams, const opencv_onnx::N
|
||||
addLayer(layerParams, node_proto);
|
||||
}
|
||||
|
||||
void ONNXImporter2::parseBitCast(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto)
|
||||
{
|
||||
CV_CheckTrue(layerParams.has("to"), "ONNXImporter2/parseBitCast: 'to' attribute is required");
|
||||
int cvtype = dataType2cv(layerParams.get<int>("to"));
|
||||
CV_CheckGE(cvtype, 0, "ONNXImporter2/parseBitCast: unsupported target datatype");
|
||||
layerParams.set("outputType", cvtype);
|
||||
layerParams.type = "BitCast";
|
||||
addLayer(layerParams, node_proto);
|
||||
}
|
||||
|
||||
void ONNXImporter2::parseConstantOfShape(LayerParams& layerParams, const opencv_onnx::NodeProto& node_proto)
|
||||
{
|
||||
layerParams.type = "ConstantOfShape";
|
||||
@@ -3157,6 +3168,7 @@ void ONNXImporter2::buildDispatchMap_ONNX_AI()
|
||||
dispatch["OneHot"] = &ONNXImporter2::parseOneHot;
|
||||
dispatch["DFT"] = &ONNXImporter2::parseDFT;
|
||||
dispatch["Det"] = &ONNXImporter2::parseDet;
|
||||
dispatch["BitCast"] = &ONNXImporter2::parseBitCast;
|
||||
dispatch["EyeLike"] = &ONNXImporter2::parseEyeLike;
|
||||
dispatch["BlackmanWindow"] = &ONNXImporter2::parseBlackmanWindow;
|
||||
dispatch["HannWindow"] = &ONNXImporter2::parseHannWindow;
|
||||
|
||||
@@ -2832,4 +2832,33 @@ TEST(Test_MatMul, FastGemmBatchDynamicAndPackedBroadcast)
|
||||
normAssert(packedOutputs[0], packedExpected, "fastGemm packed broadcast batch mismatch", 1e-4, 1e-4);
|
||||
}
|
||||
|
||||
TEST(Test_MatMul, ConstantRank1WeightPacking)
|
||||
{
|
||||
// [M, K] @ [K] -> [M] with a constant rank-1 weight, as seen in DEIMv2-style graphs.
|
||||
const int M = 4960, K = 33;
|
||||
Mat A(M, K, CV_32F);
|
||||
Mat B(std::vector<int>{K}, CV_32F); // genuinely rank-1, not [K, 1]
|
||||
randu(A, -1.f, 1.f);
|
||||
randu(B, -1.f, 1.f);
|
||||
|
||||
LayerParams lp;
|
||||
lp.type = "MatMul";
|
||||
lp.name = "matmul_constant_rank1_B";
|
||||
lp.set("transA", false);
|
||||
lp.set("transB", false);
|
||||
lp.blobs.push_back(B);
|
||||
|
||||
Ptr<Layer> layer = LayerFactory::createLayerInstance(lp.type, lp);
|
||||
ASSERT_TRUE(layer);
|
||||
std::vector<Mat> inputs = {A}, outputs;
|
||||
runLayer(layer, inputs, outputs);
|
||||
ASSERT_EQ(outputs.size(), (size_t)1);
|
||||
|
||||
Mat b2d = B.reshape(1, std::vector<int>{K, 1});
|
||||
Mat expected2d;
|
||||
gemm(A, b2d, 1., noArray(), 0., expected2d);
|
||||
Mat expected = expected2d.reshape(1, std::vector<int>{M});
|
||||
normAssert(outputs[0], expected, "MatMul constant rank-1 weight packing mismatch", 1e-4, 1e-4);
|
||||
}
|
||||
|
||||
}} // namespace
|
||||
|
||||
@@ -3732,6 +3732,36 @@ CASE(test_matmul_bcast)
|
||||
SKIP;
|
||||
CASE(test_scatter_elements_with_reduction_mul)
|
||||
SKIP;
|
||||
CASE(test_bitcast_2d_float32_to_int32)
|
||||
SKIP;
|
||||
CASE(test_bitcast_bool_to_uint8)
|
||||
SKIP;
|
||||
CASE(test_bitcast_float32_to_int32)
|
||||
SKIP;
|
||||
CASE(test_bitcast_float64_to_int64)
|
||||
SKIP;
|
||||
CASE(test_bitcast_int32_to_float32)
|
||||
SKIP;
|
||||
CASE(test_bitcast_int64_to_float64)
|
||||
SKIP;
|
||||
CASE(test_bitcast_int8_to_uint8)
|
||||
SKIP;
|
||||
CASE(test_bitcast_scalar_float32_to_int32)
|
||||
SKIP;
|
||||
CASE(test_bitcast_uint16_to_int16)
|
||||
SKIP;
|
||||
CASE(test_bitcast_uint32_to_int32)
|
||||
SKIP;
|
||||
CASE(test_matmul_1d_1d)
|
||||
SKIP;
|
||||
CASE(test_matmul_1d_3d)
|
||||
SKIP;
|
||||
CASE(test_matmul_4d_1d)
|
||||
SKIP;
|
||||
CASE(test_dft_irfft)
|
||||
SKIP;
|
||||
CASE(test_dft_irfft_opset19)
|
||||
SKIP;
|
||||
END_SWITCH()
|
||||
#undef EOF_LABEL
|
||||
#undef BEGIN_SWITCH
|
||||
|
||||
@@ -216,17 +216,6 @@
|
||||
"test_training_dropout_mask", // ---- same as above ---
|
||||
|
||||
// ===== ONNX 1.22 additions: ops/dtypes not yet supported by the importer =====
|
||||
// BitCast op not supported by the ONNX importer
|
||||
"test_bitcast_2d_float32_to_int32",
|
||||
"test_bitcast_bool_to_uint8",
|
||||
"test_bitcast_float32_to_int32",
|
||||
"test_bitcast_float64_to_int64",
|
||||
"test_bitcast_int32_to_float32",
|
||||
"test_bitcast_int64_to_float64",
|
||||
"test_bitcast_int8_to_uint8",
|
||||
"test_bitcast_scalar_float32_to_int32",
|
||||
"test_bitcast_uint16_to_int16",
|
||||
"test_bitcast_uint32_to_int32",
|
||||
// INT2/UINT2 (2-bit) dtype not supported
|
||||
"test_cast_FLOAT16_to_INT2",
|
||||
"test_cast_FLOAT16_to_UINT2",
|
||||
@@ -268,13 +257,6 @@
|
||||
"test_range_bfloat16_type_positive_delta_expanded",
|
||||
"test_range_float16_type_positive_delta_expanded",
|
||||
// ===== ONNX 1.22 additions: forward/accuracy not yet supported =====
|
||||
// MatMul with 1-D operand not supported (requires >=2D)
|
||||
"test_matmul_1d_1d",
|
||||
"test_matmul_1d_3d",
|
||||
"test_matmul_4d_1d",
|
||||
// DFT inverse RFFT not supported
|
||||
"test_dft_irfft",
|
||||
"test_dft_irfft_opset19",
|
||||
// Attention softcap accuracy
|
||||
"test_attention_4d_softcap_neginf_mask",
|
||||
"test_attention_4d_softcap_neginf_mask_poison",
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "cv2_util.hpp"
|
||||
#include "opencv2/core/utils/logger.hpp"
|
||||
|
||||
#include <limits>
|
||||
|
||||
PyTypeObject* pyopencv_Mat_TypePtr = nullptr;
|
||||
|
||||
//======================================================================================================================
|
||||
@@ -25,6 +27,25 @@ static std::string pycv_dumpArray(const T* arr, int n)
|
||||
return out.str();
|
||||
}
|
||||
|
||||
static bool int64ArrayFitsInt32(PyArrayObject* arr)
|
||||
{
|
||||
// Not GETCONTIGUOUS: PyArray_TYPE() also reports NPY_LONGLONG for byte-swapped dtypes.
|
||||
PyArrayObject* contig = (PyArrayObject*)PyArray_FROM_OTF((PyObject*)arr, NPY_INT64, NPY_ARRAY_IN_ARRAY);
|
||||
if (!contig)
|
||||
{
|
||||
PyErr_Clear();
|
||||
return false;
|
||||
}
|
||||
const int64_t* data = (const int64_t*)PyArray_DATA(contig);
|
||||
const npy_intp total = PyArray_SIZE(contig);
|
||||
bool fits = true;
|
||||
for (npy_intp i = 0; i < total && fits; i++)
|
||||
fits = data[i] >= (int64_t)std::numeric_limits<int32_t>::min() &&
|
||||
data[i] <= (int64_t)std::numeric_limits<int32_t>::max();
|
||||
Py_DECREF(contig);
|
||||
return fits;
|
||||
}
|
||||
|
||||
static inline std::string getArrayTypeName(PyArrayObject* arr)
|
||||
{
|
||||
PyArray_Descr* dtype = PyArray_DESCR(arr);
|
||||
@@ -133,19 +154,18 @@ bool pyopencv_to(PyObject* o, Mat& m, const ArgInfo& info)
|
||||
|
||||
if( type < 0 )
|
||||
{
|
||||
if( typenum == NPY_INT64 || typenum == NPY_LONG || typenum == NPY_LONGLONG )
|
||||
{
|
||||
needcopy = needcast = true;
|
||||
new_typenum = NPY_INT;
|
||||
type = CV_32S;
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::string dtype_name = getArrayTypeName(oarr);
|
||||
failmsg("%s data type = %s is not supported", info.name,
|
||||
dtype_name.c_str());
|
||||
return false;
|
||||
}
|
||||
const std::string dtype_name = getArrayTypeName(oarr);
|
||||
failmsg("%s data type = %s is not supported", info.name,
|
||||
dtype_name.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// int64 is numpy's default int dtype: narrow for CV_32S APIs, but only losslessly.
|
||||
if( type == CV_64S && int64ArrayFitsInt32(oarr) )
|
||||
{
|
||||
needcopy = needcast = true;
|
||||
new_typenum = NPY_INT;
|
||||
type = CV_32S;
|
||||
}
|
||||
|
||||
#ifndef CV_MAX_DIM
|
||||
|
||||
@@ -42,8 +42,10 @@ int numpyTypeToCvDepth(int typenum)
|
||||
case NPY_SHORT: return CV_16S;
|
||||
case NPY_UINT: return CV_32U;
|
||||
case NPY_INT: return CV_32S;
|
||||
case NPY_LONGLONG: return CV_64S;
|
||||
case NPY_ULONGLONG: return CV_64U;
|
||||
// 'long' is 64-bit on LP64 but 32-bit on LLP64, so decide by size, not by name.
|
||||
case NPY_LONG: return NPY_SIZEOF_LONG == 8 ? CV_64S : CV_32S;
|
||||
case NPY_ULONG: return NPY_SIZEOF_LONG == 8 ? CV_64U : CV_32U;
|
||||
case NPY_HALF: return CV_16F;
|
||||
case NPY_FLOAT: return CV_32F;
|
||||
|
||||
@@ -285,6 +285,16 @@ class Arguments(NewOpenCVTests):
|
||||
res9 = cv.utils.dumpInputArray(a)
|
||||
self.assertEqual(res9, "InputArray: empty()=false kind=0x00010000 flags=0x01010000 total(-1)=1 dims(-1)=0 size(-1)=1x1 type(-1)=CV_32FC1")
|
||||
|
||||
def test_InputArray_int64(self):
|
||||
"""int64 is narrowed to CV_32S for backward compatibility, but only losslessly."""
|
||||
int32_min, int32_max = np.iinfo(np.int32).min, np.iinfo(np.int32).max
|
||||
for values in ([[1, 2], [3, 4]], [int32_min, int32_max]):
|
||||
a = np.array(values, dtype=np.int64)
|
||||
self.assertIn("type(-1)=CV_32SC1", cv.utils.dumpInputArray(a))
|
||||
for values in ([[1, 2], [3, 1 << 40]], [int32_min - 1], [int32_max + 1]):
|
||||
a = np.array(values, dtype=np.int64)
|
||||
self.assertIn("type(-1)=CV_64SC1", cv.utils.dumpInputArray(a))
|
||||
|
||||
def test_InputArrayOfArrays(self):
|
||||
res1 = cv.utils.dumpInputArrayOfArrays(None)
|
||||
# self.assertEqual(res1, "InputArray: noArray()") # not supported
|
||||
|
||||
Reference in New Issue
Block a user