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

Merge pull request #27785 from pratham-mcw:dnn-lstm-neon

dnn: added neon intrinsics implementation of fastGEMM1T function #27785

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

- This PR improves the performance of the LSTM function on ARM64 targets.
- Added a NEON intrinsics implementation of the fastGEMM1T function and enabled its use in fully connected and recurrent layers file. 
- As a result, ARM64 now benefits from vectorized matrix–vector multiplications, leading to measurable performance improvements in the LSTM layer.
- This change is limited to ARM64 and does not affect other architectures.

**Performance impact:**
- The optimization significantly improves the performance of lstm functions on ARM64 targets.
<img width="930" height="313" alt="image" src="https://github.com/user-attachments/assets/92e251cd-dc6c-4cda-9586-acc19bf16dfd" />
This commit is contained in:
pratham-mcw
2025-10-03 13:20:50 +05:30
committed by GitHub
parent bb45afec28
commit 8f3976ae97
3 changed files with 139 additions and 2 deletions
+2 -2
View File
@@ -4,8 +4,8 @@ endif()
set(the_description "Deep neural network module. It allows to load models from different frameworks and to make forward pass")
ocv_add_dispatched_file_force_all("layers/layers_common" AVX AVX2 AVX512_SKX RVV LASX)
ocv_add_dispatched_file_force_all("int8layers/layers_common" AVX2 AVX512_SKX RVV LASX)
ocv_add_dispatched_file_force_all("layers/layers_common" AVX AVX2 AVX512_SKX RVV LASX NEON)
ocv_add_dispatched_file_force_all("int8layers/layers_common" AVX2 AVX512_SKX RVV LASX NEON)
ocv_add_dispatched_file_force_all("layers/cpu_kernels/conv_block" AVX AVX2 NEON NEON_FP16)
ocv_add_dispatched_file_force_all("layers/cpu_kernels/conv_depthwise" AVX AVX2 RVV LASX)
ocv_add_dispatched_file("layers/cpu_kernels/conv_winograd_f63" AVX AVX2 NEON NEON_FP16)
@@ -53,6 +53,95 @@ void fastGEMM( const float* aptr, size_t astep, const float* bptr,
size_t bstep, float* cptr, size_t cstep,
int ma, int na, int nb );
#if !defined(CV_CPU_OPTIMIZATION_DECLARATIONS_ONLY) && CV_NEON
static const uint32_t tailMaskArray[7] = {
0u, 0u, 0u, 0u,
0xffffffffu, 0xffffffffu, 0xffffffffu
};
void fastGEMM1T( const float* vec, const float* weights,
size_t wstep, const float* bias,
float* dst, int nvecs, int vecsize )
{
int i = 0;
CV_Assert(vecsize >= 4 || vecsize == 0);
const uint32_t* tailMaskPtr = tailMaskArray + (vecsize % 4);
const uint32x4_t tailMaskU = vld1q_u32(tailMaskPtr);
const float32x4_t tailMask = vreinterpretq_f32_u32(tailMaskU);
for ( ; i <= nvecs - 4; i += 4 )
{
const float* wptr = weights + i * wstep;
float32x4_t vs0 = vdupq_n_f32(0.0f);
float32x4_t vs1 = vdupq_n_f32(0.0f);
float32x4_t vs2 = vdupq_n_f32(0.0f);
float32x4_t vs3 = vdupq_n_f32(0.0f);
int k = 0;
for ( ; k <= vecsize - 4; k += 4, wptr += 4 )
{
float32x4_t v = vld1q_f32(vec + k);
vs0 = vmlaq_f32(vs0, vld1q_f32(wptr), v);
vs1 = vmlaq_f32(vs1, vld1q_f32(wptr + wstep), v);
vs2 = vmlaq_f32(vs2, vld1q_f32(wptr + wstep * 2), v);
vs3 = vmlaq_f32(vs3, vld1q_f32(wptr + wstep * 3), v);
}
if (k != vecsize)
{
k = vecsize - 4;
wptr = weights + i * wstep + k;
float32x4_t v = vld1q_f32(vec + k);
v = vreinterpretq_f32_u32( vandq_u32(vreinterpretq_u32_f32(v), vreinterpretq_u32_f32(tailMask)) );
float32x4_t w0 = vreinterpretq_f32_u32( vandq_u32(vreinterpretq_u32_f32(vld1q_f32(wptr)), vreinterpretq_u32_f32(tailMask)) );
float32x4_t w1 = vreinterpretq_f32_u32( vandq_u32(vreinterpretq_u32_f32(vld1q_f32(wptr + wstep)), vreinterpretq_u32_f32(tailMask)) );
float32x4_t w2 = vreinterpretq_f32_u32( vandq_u32(vreinterpretq_u32_f32(vld1q_f32(wptr + wstep * 2)), vreinterpretq_u32_f32(tailMask)) );
float32x4_t w3 = vreinterpretq_f32_u32( vandq_u32(vreinterpretq_u32_f32(vld1q_f32(wptr + wstep * 3)), vreinterpretq_u32_f32(tailMask)) );
vs0 = vmlaq_f32(vs0, w0, v);
vs1 = vmlaq_f32(vs1, w1, v);
vs2 = vmlaq_f32(vs2, w2, v);
vs3 = vmlaq_f32(vs3, w3, v);
}
auto hsumq_f32 = [](float32x4_t x) -> float {
float32x2_t s2 = vadd_f32(vget_low_f32(x), vget_high_f32(x));
s2 = vpadd_f32(s2, s2);
return vget_lane_f32(s2, 0);
};
dst[i + 0] = hsumq_f32(vs0) + bias[i + 0];
dst[i + 1] = hsumq_f32(vs1) + bias[i + 1];
dst[i + 2] = hsumq_f32(vs2) + bias[i + 2];
dst[i + 3] = hsumq_f32(vs3) + bias[i + 3];
}
for ( ; i < nvecs; i++ )
{
const float* wptr = weights + i * wstep;
float32x4_t vs0 = vdupq_n_f32(0.0f);
int k = 0;
for ( ; k <= vecsize - 4; k += 4, wptr += 4 )
{
float32x4_t v = vld1q_f32(vec + k);
vs0 = vmlaq_f32(vs0, vld1q_f32(wptr), v);
}
if (k != vecsize)
{
k = vecsize - 4;
wptr = weights + i * wstep + k;
float32x4_t v = vld1q_f32(vec + k);
v = vreinterpretq_f32_u32( vandq_u32(vreinterpretq_u32_f32(v), vreinterpretq_u32_f32(tailMask)) );
float32x4_t w0 = vreinterpretq_f32_u32( vandq_u32(vreinterpretq_u32_f32(vld1q_f32(wptr)), vreinterpretq_u32_f32(tailMask)) );
vs0 = vmlaq_f32(vs0, w0, v);
}
float32x2_t s2 = vadd_f32(vget_low_f32(vs0), vget_high_f32(vs0));
s2 = vpadd_f32(s2, s2);
dst[i] = vget_lane_f32(s2, 0) + bias[i];
}
}
#endif //CV_NEON
#if !defined(CV_CPU_OPTIMIZATION_DECLARATIONS_ONLY) && CV_AVX
#if !CV_FMA3 // AVX workaround
@@ -138,6 +138,9 @@ class LSTMLayerImpl CV_FINAL : public LSTMLayer
#if CV_TRY_AVX2
bool useAVX2;
#endif
#if CV_TRY_NEON
bool useNEON;
#endif
// CUDA needs input blobs to be rearranged in a specific way, but some transformations
// in ONNXImporter are destructive, so we keep a copy.
@@ -152,6 +155,9 @@ public:
#endif
#if CV_TRY_AVX2
, useAVX2(checkHardwareSupport(CPU_AVX2))
#endif
#if CV_TRY_NEON
, useNEON(checkHardwareSupport(CPU_NEON))
#endif
{
setParamsFrom(params);
@@ -489,6 +495,14 @@ public:
&& Wh.depth() == CV_32F && hInternal.depth() == CV_32F && gates.depth() == CV_32F
&& Wh.cols >= 8;
#endif
#if CV_TRY_NEON
bool canUseNeon = gates.isContinuous() && bias.isContinuous()
&& Wx.depth() == CV_32F && gates.depth() == CV_32F
&& bias.depth() == CV_32F && Wx.cols >= 4;
bool canUseNeon_hInternal = hInternal.isContinuous() && gates.isContinuous() && bias.isContinuous()
&& Wh.depth() == CV_32F && hInternal.depth() == CV_32F && gates.depth() == CV_32F
&& Wh.cols >= 4;
#endif
int tsStart, tsEnd, tsInc;
if (reverse || i == 1) {
@@ -539,6 +553,23 @@ public:
}
}
else
#endif
#if CV_TRY_NEON
if (useNEON && canUseNeon && xCurr.isContinuous())
{
for (int n = 0; n < xCurr.rows; n++) {
opt_NEON::fastGEMM1T(
xCurr.ptr<float>(n),
Wx.ptr<float>(),
Wx.step1(),
bias.ptr<float>(),
gates.ptr<float>(n),
Wx.rows,
Wx.cols
);
}
}
else
#endif
{
gemm(xCurr, Wx, 1, gates, 0, gates, GEMM_2_T); // Wx * x_t
@@ -578,6 +609,23 @@ public:
}
}
else
#endif
#if CV_TRY_NEON
if (useNEON && canUseNeon_hInternal)
{
for (int n = 0; n < hInternal.rows; n++) {
opt_NEON::fastGEMM1T(
hInternal.ptr<float>(n),
Wh.ptr<float>(),
Wh.step1(),
gates.ptr<float>(n),
gates.ptr<float>(n),
Wh.rows,
Wh.cols
);
}
}
else
#endif
{
gemm(hInternal, Wh, 1, gates, 1, gates, GEMM_2_T); //+Wh * h_{t-1}