Files
opencv/modules/dnn/test/test_deconvolution.cpp
T
Stern e1d515a14e Merge pull request #30001 from Hosi121:perf/dnn-col2im-workspace
dnn: reduce coordinate work in CPU Deconvolution - #30001

CPU `Deconvolution` computes coordinates and checks kernel bounds for each output value. This change builds valid tap tables for each axis, reuses outer coordinates per row, and uses SIMD for consecutive inputs at horizontal stride 1 or 2. It computes input addresses once per run and reuses them for each group of four outputs. Other positions use the scalar loop. Each output keeps the same sum order. The 1×1 path adds bias one channel segment at a time.

The table compares `5.x` at `8d126c4a` with this patch at `d0d1825`. It measures layer `forward()`, including GEMM, table setup, output reconstruction, and bias. The host is a Core Ultra 7 255H, with GCC 13.3, Release, one thread, and WSL2. IPP and OpenCL are disabled.

| Input, channels in/out | Kernel, stride | Upstream | PR | Speedup |
| --- | --- | ---: | ---: | ---: |
| 128×128, 16/16 | 1×1, 1 | 3.023 ms | 0.089 ms | 34.2× |
| 128×128, 16/16 | 2×2, 2 | 76.145 ms | 0.489 ms | 155.9× |
| 64×64, 16/16 | 3×3, 1, pad 1 | 10.709 ms | 0.239 ms | 44.8× |
| 16×16×16, 8/8 | 3×3×3, 1 | 21.843 ms | 0.472 ms | 46.3× |

Each time is the median of seven process medians, with two warmup calls and 31 measured calls per process. Version order alternates. Speedup uses the unrounded times.

Each worker reuses its address lists across rows. Their measured buffer capacities are 16 bytes for the 2×2 case and 96 bytes for the 3×3 case, excluding vector objects and allocator overhead. The SIMD run table is shared by the workers and uses one `size_t` per output column.

All 56 accuracy tests (8 new, 48 existing) pass with one and four threads. The eight performance tests pass. The 768 output comparisons match the base version. The extracted helper passes ASan/UBSan checks with SIMD and scalar code. The new accuracy tests use an independent scatter reference; duplicate pointwise and dynamic-weight cases were removed.

[Test code, raw results, and build steps](https://github.com/Hosi121/opencv/tree/bench/dnn-col2im-workspace/col2im-bench).

This changes the legacy CPU `Deconvolution` path. The new ONNX `ConvTranspose2` path is separate. These are layer results, not complete-model results. ARM hardware was not tested.

English is not my first language, so I used AI to help write this post.
2026-09-28 08:54:42 +03:00

140 lines
6.1 KiB
C++

// 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 "test_precomp.hpp"
#include <opencv2/dnn/all_layers.hpp>
namespace opencv_test { namespace {
enum DeconvolutionMode { Pointwise, NonOverlap, Overlap, Dilated };
typedef TestWithParam<tuple<int, DeconvolutionMode, bool> > DeconvolutionCoordinates;
TEST_P(DeconvolutionCoordinates, Accuracy)
{
const int dims = get<0>(GetParam());
const DeconvolutionMode mode = get<1>(GetParam());
const bool dynamicWeights = get<2>(GetParam());
const int inChannels = 4, outChannels = 6, groups = 2;
const bool hasBias = mode != NonOverlap;
std::vector<int> kernel(dims), stride(dims), dilation(dims, 1);
std::vector<int> pads(2 * dims, 0), adjust(dims, 0);
int kernelSize = 1;
for (int d = 0; d < dims; ++d)
{
kernel[d] = mode == Pointwise ? 1 : mode == Overlap ? 3 : 2 + d % 2;
stride[d] = mode == NonOverlap ? kernel[d] : mode == Dilated ? 2 : 1;
dilation[d] = mode == Dilated && d == 0 ? 2 : 1;
pads[d] = mode >= Overlap ? 1 : 0;
pads[d + dims] = mode >= Overlap ? d % 2 : 0;
adjust[d] = mode == Dilated ? 1 : 0;
kernelSize *= kernel[d];
}
LayerParams lp;
lp.set("kernel_size", DictValue::arrayInt(kernel.data(), dims));
lp.set("stride", DictValue::arrayInt(stride.data(), dims));
lp.set("dilation", DictValue::arrayInt(dilation.data(), dims));
lp.set("pad", DictValue::arrayInt(pads.data(), 2 * dims));
lp.set("adj", DictValue::arrayInt(adjust.data(), dims));
lp.set("num_output", outChannels);
lp.set("group", groups);
lp.set("bias_term", hasBias);
std::vector<int> weightShape = {inChannels, outChannels / groups};
weightShape.insert(weightShape.end(), kernel.begin(), kernel.end());
Mat weights(weightShape, CV_32F), bias(1, outChannels, CV_32F);
for (size_t i = 0; i < weights.total(); ++i)
weights.ptr<float>()[i] = (int(i * 7 % 11) - 5) * 0.125f;
for (int i = 0; i < outChannels; ++i)
bias.ptr<float>()[i] = hasBias ? (i - 3) * 0.25f : 0.0f;
if (!dynamicWeights)
{
lp.blobs.push_back(weights);
if (hasBias)
lp.blobs.push_back(bias);
}
Ptr<Layer> layer = DeconvolutionLayer::create(lp);
for (int run = 0; run < 2; ++run)
{
std::vector<int> inputShape = {2 - run, inChannels};
std::vector<int> outputShape = {2 - run, outChannels};
int inputSize = 1, outputSize = 1;
for (int d = 0; d < dims; ++d)
{
int size = mode == Pointwise && run ? 1 : 3 + 2 * d + run;
// The resized row spans more than one SIMD block.
if (mode == Overlap && run && d == dims - 1)
size += 4;
const int outSize = (size - 1) * stride[d] + dilation[d] * (kernel[d] - 1)
+ 1 - pads[d] - pads[d + dims] + adjust[d];
inputShape.push_back(size);
outputShape.push_back(outSize);
inputSize *= size;
outputSize *= outSize;
}
Mat input(inputShape, CV_32F), expected(outputShape, CV_32F, Scalar::all(0));
for (size_t i = 0; i < input.total(); ++i)
input.ptr<float>()[i] = (int((i * 13 + run) % 17) - 8) * 0.125f;
// Scatter input values to the output. All values are exact binary fractions.
for (int n = 0; n < inputShape[0]; ++n)
for (int c = 0; c < inChannels; ++c)
for (int pos = 0; pos < inputSize; ++pos)
for (int k = 0; k < kernelSize; ++k)
{
int inputPos = pos, kernelPos = k, outputPos = 0, pitch = 1;
bool valid = true;
for (int d = dims - 1; d >= 0; --d)
{
const int coordinate = (inputPos % inputShape[d + 2]) * stride[d]
- pads[d] + (kernelPos % kernel[d]) * dilation[d];
valid = valid && coordinate >= 0 && coordinate < outputShape[d + 2];
outputPos += coordinate * pitch;
pitch *= outputShape[d + 2];
inputPos /= inputShape[d + 2];
kernelPos /= kernel[d];
}
if (!valid)
continue;
for (int q = 0; q < outChannels / groups; ++q)
{
const int outputChannel = c / (inChannels / groups) * (outChannels / groups) + q;
expected.ptr<float>()[(n * outChannels + outputChannel) * outputSize + outputPos] +=
input.ptr<float>()[(n * inChannels + c) * inputSize + pos] *
weights.ptr<float>()[(c * (outChannels / groups) + q) * kernelSize + k];
}
}
for (int n = 0; n < inputShape[0]; ++n)
for (int c = 0; c < outChannels; ++c)
for (int pos = 0; pos < outputSize; ++pos)
expected.ptr<float>()[(n * outChannels + c) * outputSize + pos] += bias.ptr<float>()[c];
std::vector<Mat> inputs(1, input), outputs;
if (dynamicWeights)
{
inputs.push_back(weights);
if (hasBias)
inputs.push_back(bias);
}
runLayer(layer, inputs, outputs);
ASSERT_EQ(outputs.size(), size_t(1));
ASSERT_EQ(outputs[0].shape(), expected.shape());
EXPECT_TRUE(cv::checkRange(outputs[0]));
EXPECT_EQ(cv::norm(outputs[0], expected, NORM_INF), 0.0);
}
}
INSTANTIATE_TEST_CASE_P(Layer_Test, DeconvolutionCoordinates, testing::Values(
make_tuple(2, Pointwise, false),
make_tuple(2, NonOverlap, false),
make_tuple(2, Overlap, false),
make_tuple(2, Dilated, false),
make_tuple(3, NonOverlap, false),
make_tuple(3, Overlap, false),
make_tuple(3, Dilated, false),
make_tuple(2, Overlap, true)));
}} // namespace