Aidge demonstration#

Aidge is a collaborative open source deep learning library optimized for exporting and processing deep learning algorithms on embedded devices. With Aidge, one you can create or import a computational graph from popular frameworks, apply modification to its structure, train it and export its architecture to various embedded devices. Aidge provides optimized functions for both inference and training, as well as many custom functionalities for the target device.

This notebook put in perspective the tool chain to import a Deep Neural Network from ONNX model and support its Inference in Aidge. The tool chain demonstrated is:

pipeline(0)

In order to demonstrate this toolchain, the MNIST digit recognition task is used.

MNIST

Setting up the notebook#

[1]:
# First import some utility methods used in the tutorial:
import sys, os
sys.path.append(os.path.abspath(os.path.join('..')))
import tuto_utils
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 4
      2 import sys, os
      3 sys.path.append(os.path.abspath(os.path.join('..')))
----> 4 import tuto_utils

ModuleNotFoundError: No module named 'tuto_utils'

(if needed) Download the model#

If you don’t have git-lfs, you can download the model and data using this piece of code

[2]:
# Download onnx model file
tuto_utils.download_material("101_first_step", "MLP_MNIST.onnx")
# Download input data
tuto_utils.download_material("101_first_step", "input_digit.npy")
# Download output data for later comparison
tuto_utils.download_material("101_first_step", "output_digit.npy")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[2], line 2
      1 # Download onnx model file
----> 2 tuto_utils.download_material("101_first_step", "MLP_MNIST.onnx")
      3 # Download input data
      4 tuto_utils.download_material("101_first_step", "input_digit.npy")

NameError: name 'tuto_utils' is not defined

Import Aidge#

In order to provide a colaborative environnement in the platform, the structure of Aidge is built on a core library that interfaces with multiple modules binded to python libraries.

  • aidge_core is the core library which offers all the basic functionnalities to create and manipulate the internal computational graph representation;

  • aidge_backend_cpu is a C++ module providing a generic C++ implementations for each component of the computational graph;

  • aidge_onnx is a module allowing to import ONNX to the Aidge framework;

  • aidge_export_cpp is a module dedicated to the generation of optimized C++ code.

This way, aidge_core is free of any dependencies and the user can install whatever they want depending on their use case.

modular

[3]:
import aidge_core

# Conv2D Operator is available but only the "export_serialize" is available.
# This backend allow to generate C++ code but not to run inference.
# For this we would need "cpu" backend.
print(f"Available backends:\n{aidge_core.get_keys_Conv2DOp()}")

# note: Tensor is a special case as 'cpu' backend is provided in the core
# module to guarantee basic functionalities such as data accesss
print(f"Available backends for Tensor:\n{aidge_core.Tensor.get_available_backends()}")
Available backends:
{'export_serialize'}
Available backends for Tensor:
{'cpu'}

As one can see, only an export backend is available for the class Conv2D, which is export_serialize. One needs to import the aidge_backend_cpu module which automatically registers itself to aidge_core, giving access to a backend that is able to run an inference.

[4]:
import aidge_backend_cpu

print(f"Available backends:\n{aidge_core.get_keys_Conv2DOp()}")

Available backends:
{'export_serialize', 'cpu'}

For this tutorial, we will need to import aidge_onnx in order to load ONNX files, numpy in order to load data and matplotlib to display images.

[5]:

import aidge_onnx import numpy as np import matplotlib.pyplot as plt

ONNX Import#

pipeline(1)

[6]:
model = aidge_onnx.load_onnx("MLP_MNIST.onnx")
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[6], line 1
----> 1 model = aidge_onnx.load_onnx("MLP_MNIST.onnx")

File /builds/eclipse/aidge/aidge/venv/lib/python3.10/site-packages/aidge_onnx/onnx_import.py:39, in load_onnx(filename, verbose)
     36 aidge_core.Log.info(f"Loading ONNX {filename}")
     38 # Load the ONNX model
---> 39 model = onnx.load(filename)
     40 return _load_onnx2graphview(model, verbose)

File /builds/eclipse/aidge/aidge/venv/lib/python3.10/site-packages/onnx/__init__.py:212, in load_model(f, format, load_external_data)
    191 def load_model(
    192     f: IO[bytes] | str | os.PathLike,
    193     format: _SupportedFormat | None = None,  # noqa: A002
    194     load_external_data: bool = True,
    195 ) -> ModelProto:
    196     """Loads a serialized ModelProto into memory.
    197
    198     Args:
   (...)
    210         Loaded in-memory ModelProto.
    211     """
--> 212     model = _get_serializer(format, f).deserialize_proto(_load_bytes(f), ModelProto())
    214     if load_external_data:
    215         model_filepath = _get_file_path(f)

File /builds/eclipse/aidge/aidge/venv/lib/python3.10/site-packages/onnx/__init__.py:149, in _load_bytes(f)
    147 else:
    148     f = typing.cast(Union[str, os.PathLike], f)
--> 149     with open(f, "rb") as readable:
    150         content = readable.read()
    151 return content

FileNotFoundError: [Errno 2] No such file or directory: 'MLP_MNIST.onnx'

As you can see in the logs, aidge imported a Node as a GenericOperator:

- /Flatten_output_0 (Flatten | GenericOperator)

This is a fallback mechanism which allows Aidge to load the entirety of an ONNX graph without failing, even when encountering a node that is not yet available in Aidge.

The GenericOperator acts as a stub retrieving the node’s type and attributes from ONNX. This allows the user to provide these nodes an implementation in a user script or remove/replace them using Aidge’s recipes, as detailed hereafter. You can visualize the graph using the save method and the mermaid visualizer we have setup.

[7]:
model.save("myModel")
tuto_utils.visualize_mmd("myModel.mmd")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 1
----> 1 model.save("myModel")
      2 tuto_utils.visualize_mmd("myModel.mmd")

NameError: name 'model' is not defined

Graph transformation#

pipeline(2)

In order to support the graph for inference we need to support all operators. The imported model contains Flatten before the Gemm operator. The aidge.FC operator already supports the flatten operation. Graph transformation is required to support the graph for inference, i.e. remove the Flatten operator.

Aidge’s graph transformation toolchain is embedded inside recipes functions. These recipes are available in aidge_core.

Examples include:

  • fuse_batchnorm: Fuse BatchNorm inside Conv or FC operator;

  • matmul_to_fc: Fuse MatMul and Add operator into a FC operator;

  • conv_horizontal_tiling: replace a conv by an horizontal tilled version;

  • remove_flatten: Remove Flatten if it is before an FC operator;

  • adapt_to_backend: Adapt graph to the current backend by adding Transpose layer to match expected input/output data format;

  • constant_folding: Compute constant part of the graph and replace them by pre-computed values.

Let’s apply the remove_flatten recipe:

[8]:
# Use the remove_flatten recipe
aidge_core.remove_flatten(model)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[8], line 2
      1 # Use the remove_flatten recipe
----> 2 aidge_core.remove_flatten(model)

NameError: name 'model' is not defined

The flatten node is removed with the recipie. Let’s visualize the model:

[9]:
model.save("mySupportedModel")
tuto_utils.visualize_mmd("mySupportedModel.mmd")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 model.save("mySupportedModel")
      2 tuto_utils.visualize_mmd("mySupportedModel.mmd")

NameError: name 'model' is not defined

Static analysis#

pipeline(3)

Static analysis can be applied anytime on a graph in order to measure its complexity in terms of memory and operations.

[10]:
import aidge_core.static_analysis

# Dims must be forwarded for static analysis!
model.forward_dims(dims=[[1, 1, 28, 28]], allow_data_dependency=True)

model_stats = aidge_core.static_analysis.StaticAnalysis(model)
model_stats.summary()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[10], line 4
      1 import aidge_core.static_analysis
      3 # Dims must be forwarded for static analysis!
----> 4 model.forward_dims(dims=[[1, 1, 28, 28]], allow_data_dependency=True)
      6 model_stats = aidge_core.static_analysis.StaticAnalysis(model)
      7 model_stats.summary()

NameError: name 'model' is not defined
[11]:
model_stats.log_nb_ops_by_type("stats_ops.png", log_scale=True)

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[11], line 1
----> 1 model_stats.log_nb_ops_by_type("stats_ops.png", log_scale=True)

NameError: name 'model_stats' is not defined

Inference#

pipeline(4)

Create an input tensor#

In order to perform an inference pass, we will load an image from the MNIST dataset using Numpy.

[12]:
## Load input data & its output from the MNIST_model
digit = np.load("input_digit.npy")
plt.imshow(digit[0][0], cmap='gray')

---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[12], line 2
      1 ## Load input data & its output from the MNIST_model
----> 2 digit = np.load("input_digit.npy")
      3 plt.imshow(digit[0][0], cmap='gray')

File /builds/eclipse/aidge/aidge/venv/lib/python3.10/site-packages/numpy/lib/_npyio_impl.py:451, in load(file, mmap_mode, allow_pickle, fix_imports, encoding, max_header_size)
    449     own_fid = False
    450 else:
--> 451     fid = stack.enter_context(open(os.fspath(file), "rb"))
    452     own_fid = True
    454 # Code to distinguish from NumPy binary files and pickles.

FileNotFoundError: [Errno 2] No such file or directory: 'input_digit.npy'

And in order to validate the result our model will provide, we will also load the output the PyTorch model povided for this image

[13]:
output_model = np.load("output_digit.npy")
print(output_model)

---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[13], line 1
----> 1 output_model = np.load("output_digit.npy")
      2 print(output_model)

File /builds/eclipse/aidge/aidge/venv/lib/python3.10/site-packages/numpy/lib/_npyio_impl.py:451, in load(file, mmap_mode, allow_pickle, fix_imports, encoding, max_header_size)
    449     own_fid = False
    450 else:
--> 451     fid = stack.enter_context(open(os.fspath(file), "rb"))
    452     own_fid = True
    454 # Code to distinguish from NumPy binary files and pickles.

FileNotFoundError: [Errno 2] No such file or directory: 'output_digit.npy'

Thanks to the Numpy interoperability we can create an Aidge Tensor using directly the numpy array storing the image.

[14]:
input_tensor = aidge_core.Tensor(digit)
print(f"Aidge Input Tensor dimensions: \n{input_tensor.dims()}")

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[14], line 1
----> 1 input_tensor = aidge_core.Tensor(digit)
      2 print(f"Aidge Input Tensor dimensions: \n{input_tensor.dims()}")

NameError: name 'digit' is not defined

Configure the model for inference#

At the moment the model has no implementation, it is only a datastructure. To set an implementation we will set a dataype and a backend.

[15]:
# Configure the model
model.compile("cpu", aidge_core.dtype.float32, dims=[[1,1,28,28]])
# equivalent to set_datatype(), set_backend() and forward_dims()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[15], line 2
      1 # Configure the model
----> 2 model.compile("cpu", aidge_core.dtype.float32, dims=[[1,1,28,28]])
      3 # equivalent to set_datatype(), set_backend() and forward_dims()

NameError: name 'model' is not defined

Create a scheduler and run inference#

The graph is ready to run ! We just need to schedule the execution, to do this we will create a Scheduler object, which will take the graph and generate an optimized scheduling using a consummer producer heuristic.

[16]:
# Create SCHEDULER
scheduler = aidge_core.SequentialScheduler(model)

# Run inference !
scheduler.forward(data=[input_tensor])

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[16], line 2
      1 # Create SCHEDULER
----> 2 scheduler = aidge_core.SequentialScheduler(model)
      4 # Run inference !
      5 scheduler.forward(data=[input_tensor])

NameError: name 'model' is not defined
[17]:
# Assert results
for outNode in model.get_output_nodes():
    output_aidge = np.array(outNode.get_operator().get_output(0))
    print(output_aidge)
    print('Aidge prediction = ', np.argmax(output_aidge[0]))
    assert(np.allclose(output_aidge, output_model,rtol=1e-04))

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[17], line 2
      1 # Assert results
----> 2 for outNode in model.get_output_nodes():
      3     output_aidge = np.array(outNode.get_operator().get_output(0))
      4     print(output_aidge)

NameError: name 'model' is not defined

It is possible to save the scheduling in a mermaid format using:

[18]:
scheduler.save_scheduling_diagram("schedulingSequential")
tuto_utils.visualize_mmd("schedulingSequential_forward.mmd")

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[18], line 1
----> 1 scheduler.save_scheduling_diagram("schedulingSequential")
      2 tuto_utils.visualize_mmd("schedulingSequential_forward.mmd")

NameError: name 'scheduler' is not defined

Optimize network#

pipeline(5)

[19]:
quantized_model = model.clone()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[19], line 1
----> 1 quantized_model = model.clone()

NameError: name 'model' is not defined
[20]:
import gzip

NB_SAMPLES = 100 # Number of samples to use for PTQ

# Use data stored in PTQ tutorial, make sure to download them using git lfs
samples = np.load(gzip.GzipFile('../PTQ_tutorial/mnist_samples.npy.gz', "r"))
for i in range(10):
    plt.subplot(1, 10, i + 1)
    plt.axis('off')
    plt.tight_layout()
    plt.imshow(samples[i], cmap='gray')

tensors = []
for sample in samples[0:NB_SAMPLES]:
    sample = np.reshape(sample, (1, 1, 28, 28)).astype(np.float32)
    tensor = aidge_core.Tensor(sample)
    tensors.append(tensor)
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[20], line 6
      3 NB_SAMPLES = 100 # Number of samples to use for PTQ
      5 # Use data stored in PTQ tutorial, make sure to download them using git lfs
----> 6 samples = np.load(gzip.GzipFile('../PTQ_tutorial/mnist_samples.npy.gz', "r"))
      7 for i in range(10):
      8     plt.subplot(1, 10, i + 1)

File /usr/lib/python3.10/gzip.py:174, in GzipFile.__init__(self, filename, mode, compresslevel, fileobj, mtime)
    172     mode += 'b'
    173 if fileobj is None:
--> 174     fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
    175 if filename is None:
    176     filename = getattr(fileobj, 'name', '')

FileNotFoundError: [Errno 2] No such file or directory: '../PTQ_tutorial/mnist_samples.npy.gz'
[21]:
import aidge_quantization
aidge_quantization.quantize_network(
    quantized_model,
    8,
    tensors,
    target_type     = aidge_core.dtype.float32,
    clipping_mode   = aidge_quantization.Clipping.MSE,
    no_quant        = False,
    optimize_signs  = True,
    single_shift    = False,
    use_cuda        = False)

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[21], line 3
      1 import aidge_quantization
      2 aidge_quantization.quantize_network(
----> 3     quantized_model,
      4     8,
      5     tensors,
      6     target_type     = aidge_core.dtype.float32,
      7     clipping_mode   = aidge_quantization.Clipping.MSE,
      8     no_quant        = False,
      9     optimize_signs  = True,
     10     single_shift    = False,
     11     use_cuda        = False)

NameError: name 'quantized_model' is not defined
[22]:
quantized_model.save("quantizedModel")
tuto_utils.visualize_mmd("quantizedModel.mmd")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[22], line 1
----> 1 quantized_model.save("quantizedModel")
      2 tuto_utils.visualize_mmd("quantizedModel.mmd")

NameError: name 'quantized_model' is not defined

Export#

Now that we have tested the imported graph we can look at one of the main feature of Aidge, the export of computationnal graph to an hardware target using code generation.

pipeline(6)

Generate an export in C++#

In this example we will generate a generic C++ export. This export is not based on the cpu backend we have set before.

In this example we will create a standalone export which is abstracted from the Aidge platform.

[23]:
! rm -r myexport

rm: cannot remove 'myexport': No such file or directory
[24]:
!ls myexport

ls: cannot access 'myexport': No such file or directory

Generating a cpu export recquires the aidge_export_cpp module.

Once the module is imported you just need one line to generate an export of the graph.

[25]:
import aidge_export_cpp

# Configuration for the model + forward dimensions
model.compile("cpu", aidge_core.dtype.float32, dims=[[1, 1, 28, 28]])
# Export the model in C++ standalone
aidge_core.export_utils.scheduler_export(
        scheduler,
        "myexport",
        aidge_export_cpp.ExportLibCpp,
        memory_manager=aidge_core.mem_info.generate_optimized_memory_info,
        memory_manager_args={"stats_folder": "myexport/stats", "wrapping": False }
)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[25], line 4
      1 import aidge_export_cpp
      3 # Configuration for the model + forward dimensions
----> 4 model.compile("cpu", aidge_core.dtype.float32, dims=[[1, 1, 28, 28]])
      5 # Export the model in C++ standalone
      6 aidge_core.export_utils.scheduler_export(
      7         scheduler,
      8         "myexport",
   (...)
     11         memory_manager_args={"stats_folder": "myexport/stats", "wrapping": False }
     12 )

NameError: name 'model' is not defined

The export_scheduler function will generate:

  • dnn/include/forward.hpp define API function to use the export;

  • dnn/include/kernels folders for kernels;

  • dnn/include/layers layers configuration;

  • dnn/include/parameters folder with parameters;

  • dnn/src/forward.cpp source code of forward function which call kernels;

  • Makefile To compile the main.cpp

[26]:
!tree myexport

/usr/bin/sh: 1: tree: not found

Generate main file#

Export scheduler only generates the export of the kernels and a forward function which calls the kernels in the order described by the scheduler.

From this point we can start building an application. In order to do so, Aidge proposes a utils function named generate_main_cpp, which generates a simple main.cpp, able to perform an inference pass based on an input tensor provided by the user.

[27]:
aidge_core.export_utils.generate_main_cpp("myexport", model)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[27], line 1
----> 1 aidge_core.export_utils.generate_main_cpp("myexport", model)

NameError: name 'model' is not defined
[28]:
!cat myexport/main.cpp
cat: myexport/main.cpp: No such file or directory

(Optional) Generate an input file for tests#

To test the export we need to provide input data. The generate_main_cpp function automatically generates an input file using tensor set as an input.

This is the case here, has we set an input tensor when running the forward pass, so we don’t need to execute the following cell. However, if no input has been set you need to manually generate the input file, to do so we can export a Numpy array using:

aidge_core.export_utils.generate_input_file(export_folder="myexport", array_name="fc1_Gemm_input_0", tensor=aidge_core.Tensor(digit.reshape(-1)))

Compile the export#

Once the generation has been done, we can compile the export with a simple make command:

[29]:
!cd myexport && make

/usr/bin/sh: 1: cd: can't cd to myexport

Run the export#

[30]:
!./myexport/bin/run_export

/usr/bin/sh: 1: ./myexport/bin/run_export: not found