From c02b096a77f934c77a293e543284bedd9fd6ad3f Mon Sep 17 00:00:00 2001 From: velonica0 <47554626+velonica0@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:37:27 +0800 Subject: [PATCH] Merge pull request #29357 from velonica0:dnn_conv_rvv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dnn: vectorize fp32 convolution on RISC-V RVV #29357 ### Summary Vectorizes the new DNN engine's fp32 convolution on RISC-V **RVV**. Convolution was the last major block-layout operator still running scalar on RVV; this brings it to parity with the already-vectorized depthwise/pool/batchnorm path. Single file, **purely additive** (`#elif CV_SIMD_SCALABLE` branches only) — x86/AVX2, ARM/NEON and AArch64 codegen are unchanged. ### Background - **#28585** introduced block-layout conv with fp32 kernels specialized for AVX2 (`C0=8`) and NEON, scalar `#else` for everything else, and explicitly noted *"we could add the respective kernels later."* - On RVV that `#else` meant conv ran **scalar at every VLEN** — the C0=8 fixed width approach doesn't transfer to a variable-VLEN ISA. - **#29304** made the block size track the hardware width (`C0 = vlanes()`), fixing the VLEN≥256 assertion but leaving conv scalar. - **This PR** supplies the deferred RVV conv vectorization, on top of that `C0=vlanes()` foundation. ### Approach - Re-enable the `SPAT_BLOCK_SIZE=10` **blocked path** on RVV. Because `K0 == vlanes()`, one `v_float32` accumulator covers a full output-channel block, so 10 accumulators process 10 output positions together. A single K0-wide weight `vx_load` is reused across all 10 — amortizing the per-output-point load that bottlenecks the scalar path (this is what enables multi-core scaling; the scalar/per-FMA-load path is bandwidth-bound). - Vectorize the **scalar tail** (the `<10`-position remainder) the same way. - **Vector-length-agnostic:** `C0` is runtime, the kernel is written in terms of `v_float32`/`vlanes()`, so the same binary runs at full width on VLEN 128/256/512/1024 with no recompile. No per-VLEN kernels. - The six `C0=8` specialized kernels stay `#if !CV_SIMD_SCALABLE`-gated (they can't run at `C0=vlanes()`); on RVV all shapes use the generic kernel. ### Performance Convolution throughput, blocked (this PR) vs scalar, same machine/engine (VLEN=256, 8× rv64 @ 2.4 GHz), GFLOP/s: | layer | scalar 1T | this 1T | scalar 8T | this 8T | 8T speedup | |---|---|---|---|---|---| | 3×3, 256→256, 64² | 1.13 | 13.23 | 8.28 | 75.62 | **9.1×** | | 1×1, 256→256, 64² | 1.08 | 11.73 | 8.02 | 71.70 | 8.9× | | 3×3, 128→128, 128² | 1.10 | 13.02 | 8.45 | 89.14 | **10.6×** | ≈ **12× single-thread, 9–11× at 8 threads**. Convolution dominates CNN inference time, so this is a large end-to-end win on RVV hardware. ### Validation K3-class RVV board, VLEN=256: `Test_ONNX_layers` **264/264**, `Test_ONNX_conformance` **1641/1641**, **0 assertions**. Output is bit-identical to the scalar path (same accumulation order, just vectorized over `K0`). Vector-length-agnosticism confirmed by running the VLEN=256 binary unchanged at **VLEN=1024**. ### 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 --- .../layers/cpu_kernels/conv2_kernels.simd.hpp | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/modules/dnn/src/layers/cpu_kernels/conv2_kernels.simd.hpp b/modules/dnn/src/layers/cpu_kernels/conv2_kernels.simd.hpp index 65adfa8547..c49f02b24d 100644 --- a/modules/dnn/src/layers/cpu_kernels/conv2_kernels.simd.hpp +++ b/modules/dnn/src/layers/cpu_kernels/conv2_kernels.simd.hpp @@ -386,6 +386,82 @@ CV_CPU_OPTIMIZATION_NAMESPACE_BEGIN CONV_FINALIZE_OUT2(8, 9, CONV_ADD_NO_RESIDUAL2); \ } +#elif CV_SIMD_SCALABLE + +/////////////////////////// scalable (RVV) implementation ///////////////////////////// +// K0 == vlanes(), so each of the 10 spatial positions needs exactly one vector +// accumulator (structurally identical to the AVX2 path, which uses one v_float32x8 per +// position for its K0=8). One K0-wide weight load is reused across all 10 positions, so +// the per-output-point load bottleneck of the scalar tail is amortized away. C0 (= vlanes()) +// is a runtime value, so the c0 sweep is a loop rather than a fixed unroll. + +#undef CONV_INIT_SUMS +#define CONV_INIT_SUMS() \ + v_float32 zz = vx_setzero_f32(); \ + v_float32 s0 = zz, s1 = zz, s2 = zz, s3 = zz, s4 = zz; \ + v_float32 s5 = zz, s6 = zz, s7 = zz, s8 = zz, s9 = zz + +#undef CONV_UPDATE_LOOP_BODY +#define CONV_UPDATE_LOOP_BODY() \ + for (int _c0 = 0; _c0 < C0; _c0++) { \ + v_float32 _w = vx_load(wptr + _c0*K0); \ + s0 = v_fma(vx_setall_f32(inptr[0][_c0]), _w, s0); \ + s1 = v_fma(vx_setall_f32(inptr[1][_c0]), _w, s1); \ + s2 = v_fma(vx_setall_f32(inptr[2][_c0]), _w, s2); \ + s3 = v_fma(vx_setall_f32(inptr[3][_c0]), _w, s3); \ + s4 = v_fma(vx_setall_f32(inptr[4][_c0]), _w, s4); \ + s5 = v_fma(vx_setall_f32(inptr[5][_c0]), _w, s5); \ + s6 = v_fma(vx_setall_f32(inptr[6][_c0]), _w, s6); \ + s7 = v_fma(vx_setall_f32(inptr[7][_c0]), _w, s7); \ + s8 = v_fma(vx_setall_f32(inptr[8][_c0]), _w, s8); \ + s9 = v_fma(vx_setall_f32(inptr[9][_c0]), _w, s9); \ + } \ + inptr[0] += inpstep[0]; inptr[1] += inpstep[1]; \ + inptr[2] += inpstep[2]; inptr[3] += inpstep[3]; \ + inptr[4] += inpstep[4]; inptr[5] += inpstep[5]; \ + inptr[6] += inpstep[6]; inptr[7] += inpstep[7]; \ + inptr[8] += inpstep[8]; inptr[9] += inpstep[9] + +#undef CONV_START_FINALIZE_OUT +#define CONV_START_FINALIZE_OUT() \ + v_float32 vscale = vx_load(scalebuf); \ + v_float32 vbias = vx_load(biasbuf); \ + v_float32 valpha = vx_load(alphabuf); \ + v_float32 vmaxval = vx_setall_f32(maxval) + +#define CONV_ADD_RESIDUAL2(idx0, idx1) \ + s##idx0 = v_add(s##idx0, vx_load(tmpbuf + idx0*K0)); \ + s##idx1 = v_add(s##idx1, vx_load(tmpbuf + idx1*K0)) + +#undef CONV_FINALIZE_OUT2 +#define CONV_FINALIZE_OUT2(idx0, idx1, add_residual2) \ + s##idx0 = v_fma(s##idx0, vscale, vbias); \ + s##idx1 = v_fma(s##idx1, vscale, vbias); \ + add_residual2(idx0, idx1); \ + s##idx0 = v_select(v_ge(s##idx0, zz), s##idx0, v_mul(s##idx0, valpha)); \ + s##idx1 = v_select(v_ge(s##idx1, zz), s##idx1, v_mul(s##idx1, valpha)); \ + s##idx0 = v_min(s##idx0, vmaxval); \ + s##idx1 = v_min(s##idx1, vmaxval); \ + v_store(outbuf + idx0*K0, s##idx0); \ + v_store(outbuf + idx1*K0, s##idx1) + +#undef CONV_FINALIZE_OUT_ALL +#define CONV_FINALIZE_OUT_ALL() \ + CONV_START_FINALIZE_OUT(); \ + if (resptr) { \ + CONV_FINALIZE_OUT2(0, 1, CONV_ADD_RESIDUAL2); \ + CONV_FINALIZE_OUT2(2, 3, CONV_ADD_RESIDUAL2); \ + CONV_FINALIZE_OUT2(4, 5, CONV_ADD_RESIDUAL2); \ + CONV_FINALIZE_OUT2(6, 7, CONV_ADD_RESIDUAL2); \ + CONV_FINALIZE_OUT2(8, 9, CONV_ADD_RESIDUAL2); \ + } else { \ + CONV_FINALIZE_OUT2(0, 1, CONV_ADD_NO_RESIDUAL2); \ + CONV_FINALIZE_OUT2(2, 3, CONV_ADD_NO_RESIDUAL2); \ + CONV_FINALIZE_OUT2(4, 5, CONV_ADD_NO_RESIDUAL2); \ + CONV_FINALIZE_OUT2(6, 7, CONV_ADD_NO_RESIDUAL2); \ + CONV_FINALIZE_OUT2(8, 9, CONV_ADD_NO_RESIDUAL2); \ + } + #else #undef CONV_ENABLE_SIMD @@ -527,6 +603,11 @@ static void scatterScalarOut(bool aligned_k, int k_base, int k_count, int K0shif #define CONV_INIT_SCALAR_SUMS() \ v_float32x4 zz = v_setzero_f32(); \ v_float32x4 s0 = zz, s1 = zz +#elif CV_SIMD_SCALABLE +// RVV: K0 == vlanes(), so a single scalable vector spans the whole output block. +#define CONV_INIT_SCALAR_SUMS() \ + v_float32 zz = vx_setzero_f32(); \ + v_float32 s0 = zz #else #define CONV_INIT_SCALAR_SUMS() \ for (int _ks = 0; _ks < K0; _ks++) tmpbuf[_ks] = 0.f @@ -561,6 +642,19 @@ static void scatterScalarOut(bool aligned_k, int k_base, int k_count, int K0shif s0 = v_min(s0, _vmx); s1 = v_min(s1, _vmx); \ v_store((outbuf), s0); v_store((outbuf) + 4, s1); \ } +#elif CV_SIMD_SCALABLE +#define CONV_FINALIZE_SCALAR_OUT(outbuf) \ + { \ + v_float32 _vsc = vx_load(scalebuf); \ + v_float32 _vbi = vx_load(biasbuf); \ + v_float32 _val = vx_load(alphabuf); \ + v_float32 _vmx = vx_setall_f32(maxval); \ + s0 = v_fma(s0, _vsc, _vbi); \ + s0 = v_add(s0, vx_load(resbuf)); \ + s0 = v_select(v_ge(s0, zz), s0, v_mul(s0, _val)); \ + s0 = v_min(s0, _vmx); \ + v_store((outbuf), s0); \ + } #else #define CONV_FINALIZE_SCALAR_OUT(outbuf) \ for (int _kk = 0; _kk < K0; _kk++) { \ @@ -2025,6 +2119,15 @@ static void conv32fC8_3x3_strided(const void* inp__, const void* residual__, voi #endif // !CV_SIMD_SCALABLE +// GCC 14.x miscompiles the scalable (RVV) blocked path of conv32fC8 at -O3 via the +// -funswitch-loops pass: it produces grossly wrong output. Confirmed broken on GCC 14.2 +// and correct on GCC 15.2 (also correct at -O1/-O2). Disable that single pass for this one +// function on affected RISC-V GCC builds (< 15) as a targeted workaround. No effect on +// x86/ARM, clang, or GCC 15+ (the pragmas are skipped there). +#if defined(__GNUC__) && !defined(__clang__) && defined(__riscv) && __GNUC__ < 15 +#pragma GCC push_options +#pragma GCC optimize("no-unswitch-loops") +#endif static void conv32fC8(const void* inp__, const void* residual__, void* out__, const ConvState& cs, const void* weights__, const float* scale__, const float* bias__) @@ -2420,6 +2523,15 @@ static void conv32fC8(const void* inp__, const void* residual__, void* out__, CONV_UPDATE_BLOCK1(5); CONV_UPDATE_BLOCK1(6); CONV_UPDATE_BLOCK1(7); + #elif CV_SIMD_SCALABLE + // RVV: K0 == vlanes(); weights are contiguous over kk for a fixed c0, + // so one vx_load gives the whole K0-wide weight vector. Broadcast the + // input lane and accumulate into the persistent block accumulator. + for (int c0 = 0; c0 < C0; ++c0) { + v_float32 w = vx_load(wptr + c0*K0); + v_float32 x = vx_setall_f32(inptr[c0]); + s0 = v_fma(x, w, s0); + } #else for (int c0 = 0; c0 < C0; ++c0) { const float xval = inptr[c0]; @@ -2438,6 +2550,9 @@ static void conv32fC8(const void* inp__, const void* residual__, void* out__, } }); } +#if defined(__GNUC__) && !defined(__clang__) && defined(__riscv) && __GNUC__ < 15 +#pragma GCC pop_options +#endif cv::dnn::ConvFunc getConvFunc_(int depth, int C0) {