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:
Abhishek Gola
2026-09-01 12:53:57 +05:30
committed by GitHub
parent 16e83222dc
commit c7dd924be3
14 changed files with 542 additions and 90 deletions

View File

@@ -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 &params);
};
class CV_EXPORTS BitCastLayer : public Layer {
public:
static Ptr<BitCastLayer> create(const LayerParams &params);
};
class CV_EXPORTS LinearAttentionLayer : public Layer {
public:
static Ptr<LinearAttentionLayer> create(const LayerParams &params);

View File

@@ -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) {

View File

@@ -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);

View 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

View File

@@ -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
{

View File

@@ -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 &params)
@@ -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];

View File

@@ -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>();

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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;

View File

@@ -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