From b64a215e71ecd02dc01bdc53f9282946328f4b9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B5nu=20Samuel?= Date: Fri, 26 Jun 2026 11:06:53 +0300 Subject: [PATCH] Merge pull request #29377 from tonuonu:perf-dnn-activation-simd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dnn: SIMD for transcendental activation layers (15 functors) 🧑‍💻🤖 #2937 # dnn: SIMD for transcendental activation layers (15 functors) 🧑‍💻🤖 ### Summary Many `BaseDefaultFunctor`-based activation functors fall back to the scalar `BaseDefaultFunctor::apply` — one `libm` call per element. This PR adds vectorized `apply()` overrides (universal intrinsics, matching the existing `MishFunctor`/`GeluFunctor`/`SigmoidFunctor` pattern, each with a scalar tail) to the **15 transcendental activations** that have a clean SIMD path: | functor | uses | | functor | uses | |---|---|---|---|---| | TanH | `v_exp` (1−2/(e²ˣ+1)) | | Asinh | `v_log`,`v_sqrt` (sign·log(\|x\|+√(x²+1))) | | Log | `v_log` | | Acosh | `v_log`,`v_sqrt` | | Erf | `v_erf` | | Atanh | `v_log` | | Exp | `v_exp` | | Softplus | `v_exp`,`v_log` | | Sin | `v_sin` | | BNLL | `v_exp`,`v_log` (max(x,0)+log1p(e^−\|x\|)) | | Cos | `v_cos` | | GeluApproximation | `v_exp` (tanh) | | Tan | `v_sin`/`v_cos` | | Sinh / Cosh | `v_exp` | Functors compile at the baseline SIMD width (SSE on x86, NEON on ARM — same as the existing SIMD functors). `Sqrt`/`Floor`/`Ceil`/`Round`/`Abs`/etc. are intentionally **not** included — those are single cheap ops the compiler already auto-vectorizes (measured `Sqrt` at 0.95×). `Asin`/`Acos`/`Atan` are omitted (no `v_atan`/`v_asin`/`v_acos` intrinsic). ### Performance Real `opencv_perf_dnn` `Layer_Activation` (added here), 8×256×128×100 = 26.2 M-elem CV_32F blob, A/B vs the scalar fallback, median speedup: | functor | M4 clang/NEON | A76 gcc/NEON | Threadripper gcc | Xeon W-2235 gcc | functor | M4 | A76 | TR | Xeon | |---|---|---|---|---|---|---|---|---|---| | TanH | 9.1× | 2.3× | 7.3× | 7.3× | Sinh | 3.3× | 3.2× | 5.2× | 5.0× | | Cos | 8.8× | 3.0× | 3.3× | 3.0× | Acosh | 3.2× | 2.9× | 2.7× | 2.6× | | Log | 7.8× | 3.0× | 2.5× | 2.4× | Cosh | 2.8× | 3.2× | 2.6× | 2.5× | | Sin | 7.8× | 3.0× | 3.4× | 2.9× | Exp | 2.4× | 2.5× | 1.4× | 1.5× | | GeluApprox | 7.6× | 2.4× | 6.6× | 5.8× | BNLL | 4.2× | 2.0× | 1.6× | 1.4× | | Atanh | 7.0× | 3.6× | 6.0× | 5.2× | Softplus | 2.0× | 2.0× | 3.3× | 2.7× | | Tan | 6.8× | 3.6× | 6.6× | 6.0× | Erf | 4.2× | 2.4× | 3.9× | 3.5× | | Asinh | 3.9× | 2.4× | 5.8× | 5.7× | | | | | | Every functor is a win on every tested out-of-order core (worst case 1.4×). On the **in-order Cortex-A55** (a55-tuned build) all 15 measure 0.985–0.99× — neutral, within noise: the SIMD polynomials are dependency-chain-bound, which an in-order pipeline can neither accelerate nor (here) slow down. So: meaningful wins on out-of-order ARM + x86, no regression on in-order ARM. ### Accuracy Uses the `v_exp`/`v_log`/`v_erf`/`v_sin`/`v_cos` polynomials OpenCV already ships and relies on (Gelu via `v_erf`, Mish/Sigmoid via `v_exp`). Verified vs scalar `libm` on 320 K random elements per functor with domain-correct inputs (Acosh x≥1, Atanh |x|<1, …): **max abs error ≤ 3e-5** (most ≤ 2e-6), zero elements exceeding rel>1e-3 & abs>1e-4. ONNX conformance and the layer accuracy tests exercise these ops. ### Pull Request Readiness Checklist - [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 (4.x) - [ ] There is a reference to the original bug report and related work — N/A (perf improvement) - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable — perf test added (`Layer_Activation`, `SANITY_CHECK_NOTHING`); accuracy covered by existing ONNX conformance/layer tests - [ ] The feature is well documented and sample code can be built with the project CMake — N/A (internal optimization, no new API) --- modules/dnn/perf/perf_layer.cpp | 37 +++ modules/dnn/src/layers/elementwise_layers.cpp | 300 ++++++++++++++++++ 2 files changed, 337 insertions(+) diff --git a/modules/dnn/perf/perf_layer.cpp b/modules/dnn/perf/perf_layer.cpp index 07d1349b2d..acc83181de 100644 --- a/modules/dnn/perf/perf_layer.cpp +++ b/modules/dnn/perf/perf_layer.cpp @@ -843,6 +843,43 @@ PERF_TEST_P_(Layer_GroupNorm, GroupNorm) } +struct Layer_Activation : public TestBaseWithParam > +{ + void test_activation(const std::vector& shape, const String& type) + { + int backendId = get<0>(GetParam()); + int targetId = get<1>(GetParam()); + Mat a(shape, CV_32FC1); + randn(a, 0.f, 1.f); + Net net; + LayerParams lp; lp.type = type; lp.name = "testLayer"; + net.addLayerToPrev(lp.name, lp.type, lp); + net.setInput(a); + net.setPreferableBackend(backendId); + net.setPreferableTarget(targetId); + Mat out = net.forward(); // warmup + TEST_CYCLE() { Mat res = net.forward(); } + SANITY_CHECK_NOTHING(); + } + int N = 8, C = 256, H = 128, W = 100; +}; +PERF_TEST_P_(Layer_Activation, Exp) { test_activation({N,C,H,W}, "Exp"); } +PERF_TEST_P_(Layer_Activation, TanH) { test_activation({N,C,H,W}, "TanH"); } +PERF_TEST_P_(Layer_Activation, Log) { test_activation({N,C,H,W}, "Log"); } +PERF_TEST_P_(Layer_Activation, Erf) { test_activation({N,C,H,W}, "Erf"); } +PERF_TEST_P_(Layer_Activation, Sin) { test_activation({N,C,H,W}, "Sin"); } +PERF_TEST_P_(Layer_Activation, Cos) { test_activation({N,C,H,W}, "Cos"); } +PERF_TEST_P_(Layer_Activation, Sinh) { test_activation({N,C,H,W}, "Sinh"); } +PERF_TEST_P_(Layer_Activation, Cosh) { test_activation({N,C,H,W}, "Cosh"); } +PERF_TEST_P_(Layer_Activation, Tan) { test_activation({N,C,H,W}, "Tan"); } +PERF_TEST_P_(Layer_Activation, Softplus) { test_activation({N,C,H,W}, "Softplus"); } +PERF_TEST_P_(Layer_Activation, BNLL) { test_activation({N,C,H,W}, "BNLL"); } +PERF_TEST_P_(Layer_Activation, Gelu) { test_activation({N,C,H,W}, "GeluApproximation"); } +PERF_TEST_P_(Layer_Activation, Asinh) { test_activation({N,C,H,W}, "Asinh"); } +PERF_TEST_P_(Layer_Activation, Acosh) { test_activation({N,C,H,W}, "Acosh"); } +PERF_TEST_P_(Layer_Activation, Atanh) { test_activation({N,C,H,W}, "Atanh"); } +INSTANTIATE_TEST_CASE_P(/**/, Layer_Activation, testing::Values(std::make_tuple(DNN_BACKEND_OPENCV, DNN_TARGET_CPU))); + INSTANTIATE_TEST_CASE_P(/**/, Layer_Slice, dnnBackendsAndTargets(false, false)); INSTANTIATE_TEST_CASE_P(/**/, Layer_NaryEltwise, testing::Values(std::make_tuple(DNN_BACKEND_OPENCV, DNN_TARGET_CPU))); #ifdef HAVE_CUDA diff --git a/modules/dnn/src/layers/elementwise_layers.cpp b/modules/dnn/src/layers/elementwise_layers.cpp index f61ec36f74..47fd061a03 100644 --- a/modules/dnn/src/layers/elementwise_layers.cpp +++ b/modules/dnn/src/layers/elementwise_layers.cpp @@ -989,6 +989,32 @@ struct GeluApproximationFunctor : public BaseDefaultFunctor::vlanes(); + v_float32 one = vx_setall_f32(1.f), two = vx_setall_f32(2.f), half = vx_setall_f32(0.5f); + v_float32 c1 = vx_setall_f32(GeluApproximationConstants::sqrt_2_pi); + v_float32 c2 = vx_setall_f32(GeluApproximationConstants::coef_sqrt_2_pi); + v_float32 lo = vx_setall_f32(-88.f), hi = vx_setall_f32(88.f); + for (; i <= len - vlanes; i += vlanes) + { + v_float32 x = vx_load(srcptr + i); + v_float32 u = v_mul(x, v_fma(v_mul(x, x), c2, c1)); + v_float32 e = v_exp(v_min(v_max(v_add(u, u), lo), hi)); + v_float32 th = v_sub(one, v_div(two, v_add(e, one))); + vx_store(dstptr + i, v_mul(v_mul(half, x), v_add(one, th))); + } +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + int64 getFLOPSPerElement() const { return 100; } }; @@ -1016,6 +1042,28 @@ struct TanHFunctor : public BaseDefaultFunctor return tanh(x); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + v_float32 one = vx_setall_f32(1.f), two = vx_setall_f32(2.f); + v_float32 lo = vx_setall_f32(-88.f), hi = vx_setall_f32(88.f); + for (; i <= len - vlanes; i += vlanes) + { + v_float32 x = vx_load(srcptr + i); + v_float32 e = v_exp(v_min(v_max(v_add(x, x), lo), hi)); // e^{2x}, clamped + vx_store(dstptr + i, v_sub(one, v_div(two, v_add(e, one)))); // 1 - 2/(e+1) + } +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -1607,6 +1655,27 @@ struct BNLLFunctor : public BaseDefaultFunctor return x > 0 ? x + log(1.f + exp(-x)) : log(1.f + exp(x)); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + v_float32 one = vx_setall_f32(1.f), z = vx_setzero_f32(); + for (; i <= len - vlanes; i += vlanes) + { + v_float32 x = vx_load(srcptr + i); + // stable single-branch form: max(x,0) + log(1 + exp(-|x|)) + vx_store(dstptr + i, v_add(v_max(x, z), v_log(v_add(one, v_exp(v_sub(z, v_abs(x))))))); + } +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -1779,6 +1848,22 @@ struct LogFunctor : public BaseDefaultFunctor return log(x); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + for (; i <= len - vlanes; i += vlanes) + vx_store(dstptr + i, v_log(vx_load(srcptr + i))); +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -1982,6 +2067,26 @@ struct AcoshFunctor : public BaseDefaultFunctor return acosh(x); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + v_float32 one = vx_setall_f32(1.f); + for (; i <= len - vlanes; i += vlanes) + { + v_float32 x = vx_load(srcptr + i); + vx_store(dstptr + i, v_log(v_add(x, v_sqrt(v_sub(v_mul(x, x), one))))); + } +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -2036,6 +2141,28 @@ struct AsinhFunctor : public BaseDefaultFunctor return asinh(x); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + v_float32 one = vx_setall_f32(1.f), z = vx_setzero_f32(); + for (; i <= len - vlanes; i += vlanes) + { + v_float32 x = vx_load(srcptr + i); + v_float32 ax = v_abs(x); + v_float32 r = v_log(v_add(ax, v_sqrt(v_fma(ax, ax, one)))); + vx_store(dstptr + i, v_select(v_lt(x, z), v_sub(z, r), r)); + } +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -2090,6 +2217,26 @@ struct AtanhFunctor : public BaseDefaultFunctor return atanh(x); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + v_float32 one = vx_setall_f32(1.f), half = vx_setall_f32(0.5f); + for (; i <= len - vlanes; i += vlanes) + { + v_float32 x = vx_load(srcptr + i); + vx_store(dstptr + i, v_mul(half, v_log(v_div(v_add(one, x), v_sub(one, x))))); + } +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -2117,6 +2264,25 @@ struct CosFunctor : public BaseDefaultFunctor return cos(x); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + for (; i <= len - vlanes; i += vlanes) + { + v_float32 x = vx_load(srcptr + i); + vx_store(dstptr + i, v_cos(x)); + } +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -2144,6 +2310,26 @@ struct CoshFunctor : public BaseDefaultFunctor return cosh(x); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + v_float32 half = vx_setall_f32(0.5f), z = vx_setzero_f32(); + for (; i <= len - vlanes; i += vlanes) + { + v_float32 x = vx_load(srcptr + i); + vx_store(dstptr + i, v_mul(half, v_add(v_exp(x), v_exp(v_sub(z, x))))); + } +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -2171,6 +2357,22 @@ struct ErfFunctor : public BaseDefaultFunctor return erf(x); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + for (; i <= len - vlanes; i += vlanes) + vx_store(dstptr + i, v_erf(vx_load(srcptr + i))); +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -2282,6 +2484,25 @@ struct SinFunctor : public BaseDefaultFunctor return sin(x); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + for (; i <= len - vlanes; i += vlanes) + { + v_float32 x = vx_load(srcptr + i); + vx_store(dstptr + i, v_sin(x)); + } +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -2309,6 +2530,26 @@ struct SinhFunctor : public BaseDefaultFunctor return sinh(x); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + v_float32 half = vx_setall_f32(0.5f), z = vx_setzero_f32(); + for (; i <= len - vlanes; i += vlanes) + { + v_float32 x = vx_load(srcptr + i); + vx_store(dstptr + i, v_mul(half, v_sub(v_exp(x), v_exp(v_sub(z, x))))); + } +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -2336,6 +2577,26 @@ struct SoftplusFunctor : public BaseDefaultFunctor return log1p(exp(x)); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + v_float32 one = vx_setall_f32(1.f); + for (; i <= len - vlanes; i += vlanes) + { + v_float32 x = vx_load(srcptr + i); + vx_store(dstptr + i, v_log(v_add(one, v_exp(x)))); + } +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -2390,6 +2651,25 @@ struct TanFunctor : public BaseDefaultFunctor return tan(x); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + for (; i <= len - vlanes; i += vlanes) + { + v_float32 x = vx_load(srcptr + i); + vx_store(dstptr + i, v_div(v_sin(x), v_cos(x))); + } +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + #ifdef HAVE_CUDA Ptr initCUDA(int target, csl::Stream stream) { @@ -2827,6 +3107,26 @@ struct ExpFunctor : public BaseDefaultFunctor return exp(normScale * x + normShift); } + void apply(const float* srcptr, float* dstptr, int stripeStart, int len, size_t planeSize, int cn0, int cn1) const + { + CV_UNUSED(stripeStart); + for (int cn = cn0; cn < cn1; cn++, srcptr += planeSize, dstptr += planeSize) + { + int i = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + const int vlanes = VTraits::vlanes(); + v_float32 vsc = vx_setall_f32(normScale), vsh = vx_setall_f32(normShift); + for (; i <= len - vlanes; i += vlanes) + { + v_float32 x = vx_load(srcptr + i); + vx_store(dstptr + i, v_exp(v_fma(x, vsc, vsh))); + } +#endif + for (; i < len; i++) + dstptr[i] = calculate(srcptr[i]); + } + } + inline void setKernelParams(ocl::Kernel& kernel) const { kernel.set(3, normScale);