Skip to content

[backend_cpu] Conv2D layer gives incorrect result on tiny example

Related to aidge_export_cpp#28 (moved)

What commit version of aidge do you use

  • aidge_backend_cpu==0.5.0
  • aidge_core==0.5.1
  • aidge_export_cpp==0.2.1
  • aidge_onnx==0.4.1

Problem description

Defining an ad-hoc Conv2D layer with small input and weight matrices results in improbable results using the CPU backend. The test matrices have a number of dimensions set to 1 (for testing purposes):

  • input: Tensor[1][1][2][1] = {{{{ 1.00000}, { 2.00000}}}} is a matrix with 2 input row (single column, channel, and batch).
  • weight: Tensor[2][1][2][1] = {{{{ 1.00000}, { 2.00000}}}, {{{ 3.00000}, { 4.00000}}}} has 2 filters, matching the input size.
  • dilation, stride, and other convolution parameters should not matter has the weights match the input in size.

There are two expected outputs, one for each application of a filter to the input. Said application is a weighted sum of the input and corresponding filter. In other words:

  • output[0] = 1*1 + 2*2 = 3.
  • output[1] = 1*3 + 2*4 = 11.
  • The backend returns 11 for both values, which should not be possible. Only one combination of input and weight can result in this value.

Using the entry point generator for comparing backends, the CPU backend should provide the Expected values:

InputNode_output_0:
Expected 11.000000 <-> Predicted 1.000000
Expected 11.000000 <-> Predicted 3.000000

Number of equal outputs: 0 / 2

Reproducible example code

The code is a reduced version of the test used for the ACETONE C backend under development.

from functools import reduce
from operator import mul

import aidge_backend_cpu  # noqa: F401
import aidge_core
import numpy as np
from aidge_core import Operator, Tensor

import aidge_export_cpp


def volume(shape: list[int]) -> int:
    """Count elements in shape."""
    return reduce(mul, shape, 1)


def declare_conv_input(shape: list[int]) -> aidge_core.Tensor:
    """Declare tensor of specified shape, ignoring batch dimensions if null."""
    n = np.arange(volume(shape), dtype=np.float32).reshape(shape)
    n += 1
    t = aidge_core.Tensor(n)
    t.set_backend("cpu")
    t.set_datatype(aidge_core.dtype.float32)
    return t


if __name__ == "__main__":
    # Convolution dimensions
    # Input height/width
    H, W = 17, 19
    # Batch
    B = 2
    # Channels
    C = 3
    D = 1
    # Kernel size
    F, G = 5, 7

    # Convolution dimensions
    # Input height/width
    H, W = 2, 1
    # Batch
    B = 1
    # Channels
    C = 1
    D = 2
    # Kernel size (H, W)
    F, G = 2, 1

    # FIXME There are many version of the convolution
    # - Conv{1,2}D
    # - Padded variants
    # - Depthwise variants
    conv_layer = aidge_core.Conv2D(
        in_channels=C,
        out_channels=D,  # TODO Related to depthwise? Should be a divisor of C?
        kernel_dims=[F, G],
        stride_dims=[1, 1],  # TODO Identify where those are used/available for template
        dilation_dims=[
            1,
            1,
        ],  # TODO Identify where those are used/available for template
        name="InputNode",
        no_bias=False,
    )

    model = aidge_core.sequential([conv_layer])

    # Set backend and datatype
    model.set_backend("cpu")
    model.set_datatype(aidge_core.dtype.float32)

    ### GENERATING SCHEDULING
    scheduler = aidge_core.SequentialScheduler(model)

    ### REFERENCE INFERENCE
    data = declare_conv_input([B, C, H, W])
    print(data, f"{B}, {C}, {H}, {W}")
    # Initialise Conv2D data
    for node in model.get_nodes():
        if node.type() == "Producer":
            assert node.get_nb_outputs() == 1
            node_operator: Operator = node.get_operator()
            assert node_operator.nb_outputs() == 1
            node_target, node_target_id = node.output(0)[0]
            if node_target.type() == "Conv2D":
                if node_target_id == 1:
                    weights: Tensor = node_operator.get_output(0)
                    w = declare_conv_input(weights.dims())
                    print(w, weights.dims())
                    node_operator.set_output(0, w)
                elif node_target_id == 2:
                    pass
            node_value = node_operator.get_output(0)
    scheduler.forward(
        data=[
            data,
        ]
    )

    ### LOG OUTPUTS AND SCHEDULING
    model.log_outputs("aidge_cpp_results")
    print("Scheduling:")
    print(scheduler.get_static_scheduling())

    ### GENERATE EXPORT
    cpp_export = aidge_export_cpp.ExportLibCpp
    for exporter, exporter_dir in [(cpp_export, "export_cpp")]:
        aidge_core.export_utils.scheduler_export(
            scheduler,
            exporter_dir,
            exporter,
            memory_manager=aidge_core.mem_info.compute_default_mem_info,
        )
        aidge_core.export_utils.generate_input_file(
            export_folder=exporter_dir,
            array_name="InputNode_input_0",
            tensor=data,
        )
        aidge_core.export_utils.generate_main_compare_cpp(
            export_folder=exporter_dir,
            graph_view=model,
        )
Edited by Benjamin Lesage