From a015333f0472a50fca20d6fae780981458595b48 Mon Sep 17 00:00:00 2001 From: Tonu Samuel Date: Sun, 28 Jun 2026 18:09:05 +0300 Subject: [PATCH] ml: SIMD for SVM kernel reductions SVM::predict is dominated by the per-feature kernel reduction over the support vectors. Vectorize the four reduction kernels in svm.cpp with universal intrinsics (two independent accumulators + scalar tail): calc_non_rbf_base (dot product), calc_rbf (squared distance), calc_intersec (min-sum) and calc_chi2. Same approach as the KNN findNearest reduction in #29380. Adds modules/ml/perf/perf_svm.cpp covering the RBF/POLY/INTER/CHI2 kernels. --- modules/ml/perf/perf_svm.cpp | 48 +++++++++++++++++++++++ modules/ml/src/svm.cpp | 76 +++++++++++++++++++++++++++--------- 2 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 modules/ml/perf/perf_svm.cpp diff --git a/modules/ml/perf/perf_svm.cpp b/modules/ml/perf/perf_svm.cpp new file mode 100644 index 0000000000..b47c367a88 --- /dev/null +++ b/modules/ml/perf/perf_svm.cpp @@ -0,0 +1,48 @@ +// 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. +#include "perf_precomp.hpp" + +namespace opencv_test { namespace { + +// SVM::predict is dominated by the per-feature kernel reduction over the support +// vectors: calc_non_rbf_base (LINEAR/POLY/SIGMOID dot product), calc_rbf +// (squared distance), calc_intersec (min-sum) and calc_chi2. Train data is kept +// non-negative so the INTER and CHI2 kernels stay in their valid domain. +typedef TestBaseWithParam< tuple > SVMPredict; // (samples, dims, kernelType) + +PERF_TEST_P(SVMPredict, kernels, testing::Combine( + testing::Values(1500), + testing::Values(512), + testing::Values((int)SVM::RBF, (int)SVM::POLY, (int)SVM::INTER, (int)SVM::CHI2))) +{ + const int nsamples = get<0>(GetParam()); + const int dims = get<1>(GetParam()); + const int kernel = get<2>(GetParam()); + const int nquery = 1000; + + Mat train(nsamples, dims, CV_32F), query(nquery, dims, CV_32F); + Mat responses(nsamples, 1, CV_32S); + RNG& rng = theRNG(); + rng.fill(train, RNG::UNIFORM, 0.f, 1.f); + rng.fill(query, RNG::UNIFORM, 0.f, 1.f); + for (int i = 0; i < nsamples; i++) + responses.at(i) = i & 1; + + Ptr svm = SVM::create(); + svm->setType(SVM::C_SVC); + svm->setKernel(kernel); + svm->setC(1); + svm->setGamma(0.1); + svm->setDegree(3); // POLY + svm->setCoef0(1); // POLY + svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 1000, 1e-3)); + svm->train(train, ROW_SAMPLE, responses); + + Mat results; + TEST_CYCLE() svm->predict(query, results); + + SANITY_CHECK_NOTHING(); +} + +}} // namespace diff --git a/modules/ml/src/svm.cpp b/modules/ml/src/svm.cpp index 9b4b0dcb04..88317f4272 100644 --- a/modules/ml/src/svm.cpp +++ b/modules/ml/src/svm.cpp @@ -41,6 +41,7 @@ //M*/ #include "precomp.hpp" +#include "opencv2/core/hal/intrin.hpp" #include #include @@ -173,9 +174,19 @@ public: { const float* sample = &vecs[j*var_count]; double s = 0; - for( k = 0; k <= var_count - 4; k += 4 ) - s += sample[k]*another[k] + sample[k+1]*another[k+1] + - sample[k+2]*another[k+2] + sample[k+3]*another[k+3]; + k = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + { + const int vl = VTraits::vlanes(); + v_float32 a0 = vx_setzero_f32(), a1 = vx_setzero_f32(); + for( ; k <= var_count - 2*vl; k += 2*vl ) + { + a0 = v_fma(vx_load(sample + k), vx_load(another + k), a0); + a1 = v_fma(vx_load(sample + k + vl), vx_load(another + k + vl), a1); + } + s = (double)v_reduce_sum(v_add(a0, a1)); + } +#endif for( ; k < var_count; k++ ) s += sample[k]*another[k]; results[j] = (Qfloat)(s*alpha + beta); @@ -228,20 +239,21 @@ public: { const float* sample = &vecs[j*var_count]; double s = 0; - - for( k = 0; k <= var_count - 4; k += 4 ) + k = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) { - double t0 = sample[k] - another[k]; - double t1 = sample[k+1] - another[k+1]; - - s += t0*t0 + t1*t1; - - t0 = sample[k+2] - another[k+2]; - t1 = sample[k+3] - another[k+3]; - - s += t0*t0 + t1*t1; + const int vl = VTraits::vlanes(); + v_float32 a0 = vx_setzero_f32(), a1 = vx_setzero_f32(); + for( ; k <= var_count - 2*vl; k += 2*vl ) + { + v_float32 d0 = v_sub(vx_load(sample + k), vx_load(another + k)); + a0 = v_fma(d0, d0, a0); + v_float32 d1 = v_sub(vx_load(sample + k + vl), vx_load(another + k + vl)); + a1 = v_fma(d1, d1, a1); + } + s = (double)v_reduce_sum(v_add(a0, a1)); } - +#endif for( ; k < var_count; k++ ) { double t0 = sample[k] - another[k]; @@ -266,9 +278,19 @@ public: { const float* sample = &vecs[j*var_count]; double s = 0; - for( k = 0; k <= var_count - 4; k += 4 ) - s += std::min(sample[k],another[k]) + std::min(sample[k+1],another[k+1]) + - std::min(sample[k+2],another[k+2]) + std::min(sample[k+3],another[k+3]); + k = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + { + const int vl = VTraits::vlanes(); + v_float32 a0 = vx_setzero_f32(), a1 = vx_setzero_f32(); + for( ; k <= var_count - 2*vl; k += 2*vl ) + { + a0 = v_add(a0, v_min(vx_load(sample + k), vx_load(another + k))); + a1 = v_add(a1, v_min(vx_load(sample + k + vl), vx_load(another + k + vl))); + } + s = (double)v_reduce_sum(v_add(a0, a1)); + } +#endif for( ; k < var_count; k++ ) s += std::min(sample[k],another[k]); results[j] = (Qfloat)(s); @@ -286,7 +308,23 @@ public: { const float* sample = &vecs[j*var_count]; double chi2 = 0; - for(k = 0 ; k < var_count; k++ ) + k = 0; +#if (CV_SIMD || CV_SIMD_SCALABLE) + { + const int vl = VTraits::vlanes(); + v_float32 acc = vx_setzero_f32(), z = vx_setzero_f32(); + for( ; k <= var_count - vl; k += vl ) + { + v_float32 a = vx_load(sample + k), b = vx_load(another + k); + v_float32 d = v_sub(a, b), dv = v_add(a, b); + v_float32 term = v_div(v_mul(d, d), dv); + term = v_select(v_eq(dv, z), z, term); // divisor==0 -> add 0 (skip) + acc = v_add(acc, term); + } + chi2 = (double)v_reduce_sum(acc); + } +#endif + for( ; k < var_count; k++ ) { double d = sample[k]-another[k]; double devisor = sample[k]+another[k];