Skip to content

[export_cpp] Conv2D layer gives incorrect result on tiny example

Note this is related to issue aidge_backend_cpu#54 (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. The exporter returns 1 which should is a not a valid weighted sum for those values.
  • output[1] = 1*3 + 2*4 = 11. The exporter returns 3.

Using the entry point generator for comparing backends, the CPP exporter should provide the Predicted 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