mirror of
https://github.com/opencv/opencv.git
synced 2026-07-30 15:53:03 +04:00
Merge pull request #29104 from abhishek-gola:sdpa
Added SDPA layer (Scaled Dot Product Attention) #29104 Merge with: https://github.com/opencv/opencv_extra/pull/1374 ### 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:
@@ -768,8 +768,6 @@ TEST(Layer_MHARoPe_Test_Accuracy_with_, Pytorch)
|
||||
normAssert(h_t_reference, outputs[0]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
TEST_P(Test_Caffe_layers, Accum)
|
||||
{
|
||||
#ifdef OPENCV_DNN_EXTERNAL_PROTOBUF
|
||||
|
||||
@@ -28,6 +28,63 @@ static std::string _tf(TString filename, bool required = true)
|
||||
return findDataFile(std::string("dnn/onnx/") + filename, required);
|
||||
}
|
||||
|
||||
static Mat sdpaReference(const Mat& Q, const Mat& KT, const Mat& V, float scale)
|
||||
{
|
||||
CV_Assert(Q.dims == 4 && KT.dims == 4 && V.dims == 4);
|
||||
const int B = Q.size[0], H = Q.size[1], S_q = Q.size[2], D = Q.size[3];
|
||||
const int S_kv = V.size[2], D_v = V.size[3];
|
||||
|
||||
const float* q = Q.ptr<float>();
|
||||
const float* kt = KT.ptr<float>();
|
||||
const float* v = V.ptr<float>();
|
||||
|
||||
int szY[3] = {B, S_q, H * D_v};
|
||||
Mat Y(3, szY, CV_32F, Scalar(0));
|
||||
float* y = Y.ptr<float>();
|
||||
|
||||
const float inv_scale = (scale > 0.f) ? 1.f / scale : 1.f;
|
||||
std::vector<float> attn(S_kv);
|
||||
|
||||
for (int b = 0; b < B; b++)
|
||||
for (int h = 0; h < H; h++)
|
||||
{
|
||||
const float* Q_bh = q + ((size_t)(b * H + h) * S_q ) * D;
|
||||
const float* KT_bh = kt + ((size_t)(b * H + h) * D ) * S_kv;
|
||||
const float* V_bh = v + ((size_t)(b * H + h) * S_kv) * D_v;
|
||||
|
||||
for (int i = 0; i < S_q; i++)
|
||||
{
|
||||
for (int j = 0; j < S_kv; j++)
|
||||
{
|
||||
float s = 0.f;
|
||||
for (int d = 0; d < D; d++)
|
||||
s += Q_bh[i * D + d] * KT_bh[d * S_kv + j];
|
||||
attn[j] = s * inv_scale;
|
||||
}
|
||||
float mx = attn[0];
|
||||
for (int j = 1; j < S_kv; j++)
|
||||
mx = std::max(mx, attn[j]);
|
||||
float sum = 0.f;
|
||||
for (int j = 0; j < S_kv; j++)
|
||||
{
|
||||
attn[j] = std::exp(attn[j] - mx);
|
||||
sum += attn[j];
|
||||
}
|
||||
const float inv_sum = 1.f / sum;
|
||||
|
||||
float* out_row = y + ((size_t)(b * S_q + i) * H + h) * D_v;
|
||||
for (int d = 0; d < D_v; d++)
|
||||
{
|
||||
float acc = 0.f;
|
||||
for (int j = 0; j < S_kv; j++)
|
||||
acc += attn[j] * V_bh[j * D_v + d];
|
||||
out_row[d] = acc * inv_sum;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Y;
|
||||
}
|
||||
|
||||
class Test_ONNX_layers : public DNNTestLayer
|
||||
{
|
||||
public:
|
||||
@@ -155,6 +212,40 @@ public:
|
||||
if (checkNoFallbacks)
|
||||
expectNoFallbacksFromIE(net);
|
||||
}
|
||||
|
||||
// Runs an SDPA ONNX model through the importer + SDPALayer and checks the
|
||||
// output against an in-test attention reference computed from the same inputs.
|
||||
void testSDPAModel(const String& basename, double l1, double lInf)
|
||||
{
|
||||
// SDPA is only handled by the new-engine ONNX importer.
|
||||
auto engine_forced = static_cast<cv::dnn::EngineType>(
|
||||
cv::utils::getConfigurationParameterSizeT("OPENCV_FORCE_DNN_ENGINE", cv::dnn::ENGINE_AUTO));
|
||||
if (engine_forced == cv::dnn::ENGINE_CLASSIC) {
|
||||
applyTestTag(CV_TEST_TAG_DNN_SKIP_PARSER);
|
||||
return;
|
||||
}
|
||||
|
||||
Mat Q = blobFromNPY(_tf("data/input_" + basename + "_0.npy"));
|
||||
Mat KT = blobFromNPY(_tf("data/input_" + basename + "_1.npy"));
|
||||
Mat V = blobFromNPY(_tf("data/input_" + basename + "_2.npy"));
|
||||
|
||||
Net net = readNetFromONNX(_tf("models/" + basename + ".onnx", required));
|
||||
ASSERT_FALSE(net.empty());
|
||||
net.setPreferableBackend(backend);
|
||||
net.setPreferableTarget(target);
|
||||
|
||||
net.setInputsNames({"0", "1", "2"});
|
||||
net.setInput(Q, "0");
|
||||
net.setInput(KT, "1");
|
||||
net.setInput(V, "2");
|
||||
Mat out = net.forward("");
|
||||
|
||||
// The models are generated with scale = sqrt(head_dim).
|
||||
Mat ref = sdpaReference(Q, KT, V, std::sqrt((float)Q.size[3]));
|
||||
|
||||
EXPECT_EQ(ref.shape(), out.shape());
|
||||
normAssert(ref, out, basename.c_str(), l1 ? l1 : default_l1, lInf ? lInf : default_lInf);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_P(Test_ONNX_layers, InstanceNorm)
|
||||
@@ -3438,6 +3529,16 @@ TEST_P(Test_ONNX_layers, PyTorchAttentionSingleHead) {
|
||||
testONNXModels("pytorch_attention_single_head");
|
||||
}
|
||||
|
||||
// Batch + multi-head, square attention (B=2, H=4, S_q=S_kv=16, D=D_v=32).
|
||||
TEST_P(Test_ONNX_layers, SDPA_MultiHead) {
|
||||
testSDPAModel("sdpa_multi_head", 1e-4, 5e-4);
|
||||
}
|
||||
// Cross-attention with asymmetric shapes (S_q=12 != S_kv=20, D=16 != D_v=24)
|
||||
// to exercise the K^T re-transpose and head-merge indexing.
|
||||
TEST_P(Test_ONNX_layers, SDPA_CrossAttention) {
|
||||
testSDPAModel("sdpa_cross_attention", 1e-4, 5e-4);
|
||||
}
|
||||
|
||||
TEST_P(Test_ONNX_layers, PyTorchUnflatten){
|
||||
testONNXModels("unflatten");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user