aboutsummaryrefslogtreecommitdiff
path: root/src/backends/README.md
blob: 60e4d0baa729939b3dca27fcb85b0dabcf7994ed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# Backend developer guide

ArmNN allows adding new backends through the 'Pluggable Backend' mechanism.

## How to add a new backend

Backends reside under [src/backends](./), in separate subfolders. For Linux builds they must have a ```backend.cmake``` file
which is read automatically by [src/backends/backends.cmake](backends.cmake). The ```backend.cmake``` file
under the backend-specific folder is then included by the main CMakeLists.txt file at the root of the
ArmNN source tree.

### The backend.cmake file

The ```backend.cmake``` has three main purposes:

1. It makes sure the artifact (a cmake OBJECT library) is linked into the ArmNN shared library by appending the name of the library to the ```armnnLibraries``` list.
2. It makes sure that the subdirectory where backend sources reside gets included into the build.
3. To include backend-specific unit tests, the object library for the unit tests needs to be added to the ```armnnUnitTestLibraries``` list.


Example ```backend.cmake``` file taken from [reference/backend.cmake](reference/backend.cmake):

```cmake
#
# Make sure the reference backend is included in the build.
# By adding the subdirectory, cmake requires the presence of CMakeLists.txt
# in the reference (backend) folder.
#
add_subdirectory(${PROJECT_SOURCE_DIR}/src/backends/reference)

#
# Add the cmake OBJECT libraries built by the reference backend to the
# list of libraries linked against the ArmNN shared library.
#
list(APPEND armnnLibraries armnnRefBackend armnnRefBackendWorkloads)

#
# Backend specific unit tests can be integrated through the
# armnnUnitTestLibraries variable. This makes sure that the
# UnitTests executable can run the backend-specific unit
# tests.
#
list(APPEND armnnUnitTestLibraries armnnRefBackendUnitTests)
```

### The CMakeLists.txt file

As described in the previous section, adding a new backend will require creating a ```CMakeLists.txt``` in
the backend folder. This follows the standard cmake conventions, and is required to build a static cmake OBJECT library
to be linked into the ArmNN shared library. As with any cmake build, the code can be structured into
subfolders and modules as the developer sees fit.

Example can be found under [reference/CMakeLists.txt](reference/CMakeLists.txt).

### The backend.mk file

ArmNN on Android uses the native Android build system. New backends are integrated by creating a
```backend.mk``` file which has a single variable called ```BACKEND_SOURCES``` listing all cpp
files to be built by the Android build system for the ArmNN shared library.

Optionally, backend-specific unit tests can be added similarly, by
appending the list of cpp files to the ```BACKEND_TEST_SOURCES``` variable.

Example taken from [reference/backend.mk](reference/backend.mk):

```make
BACKEND_SOURCES := \
        RefLayerSupport.cpp \
        RefWorkloadFactory.cpp \
        workloads/Activation.cpp \
        workloads/ElementwiseFunction.cpp \
        workloads/Broadcast.cpp \
        ...

BACKEND_TEST_SOURCES := \
        test/RefCreateWorkloadTests.cpp \
        test/RefEndToEndTests.cpp \
        test/RefJsonPrinterTests.cpp \
        ...
```

## How to add common code across backends

For multiple backends that need common code, there is support for including them in the build
similarly to the backend code. This requires adding three files under a subfolder at the same level
as the backends folders. These are:

1. common.cmake
2. common.mk
3. CMakeLists.txt

They work the same way as the backend files. The only difference between them is that
common code is built first, so the backend code can depend on them.

[aclCommon](aclCommon) is an example for this concept and you can find the corresponding files:

1. [aclCommon/common.cmake](aclCommon/common.cmake)
2. [aclCommon/common.mk](aclCommon/common.mk)
3. [aclCommon/CMakeLists.txt](aclCommon/CMakeLists.txt)

## Identifying backends

Backends are identified by a string that must be unique across backends. This string is
wrapped in the [BackendId](../../include/armnn/BackendId.hpp) object for backward compatibility
with previous ArmNN versions.

## The IBackendInternal interface

All backends need to implement the [IBackendInternal](backendsCommon/IBackendInternal.hpp) interface.
The interface functions to be implemented are:

```c++
    virtual IMemoryManagerUniquePtr CreateMemoryManager() const = 0;
    virtual IWorkloadFactoryPtr CreateWorkloadFactory(
            const IMemoryManagerSharedPtr& memoryManager = nullptr) const = 0;
    virtual IBackendContextPtr CreateBackendContext(const IRuntime::CreationOptions&) const = 0;
    virtual Optimizations GetOptimizations() const = 0;
    virtual ILayerSupportSharedPtr GetLayerSupport() const = 0;
```

The ArmNN framework then creates instances of the IBackendInternal interface with the help of the
[BackendRegistry](backendsCommon/BackendRegistry.hpp) singleton.

**Important:** the ```IBackendInternal``` object is not guaranteed to have a longer lifetime than
the objects it creates. It is only intended to be a single entry point for the factory functions it has.
The best use of this is to be a lightweight, stateless object and make no assumptions between
its lifetime and the lifetime of the objects it creates.

For each backend one needs to register a factory function that can
be retrieved using a [BackendId](../../include/armnn/BackendId.hpp).
The ArmNN framework creates the backend interfaces dynamically when
it sees fit and it keeps these objects for a short period of time. Examples:

* During optimization ArmNN needs to decide which layers are supported by the backend.
   To do this, it creates a backends and calls the ```GetLayerSupport()``` function and creates
   an ```ILayerSupport``` object to help deciding this.
* During optimization ArmNN can run backend-specific optimizations. It creates backend objects
  and calls the ```GetOptimizations()``` function and it runs them on the network.
* When the Runtime is initialized it creates an optional ```IBackendContext``` object and keeps this context alive
  for the Runtime's lifetime. It notifies this context object before and after a network is loaded or unloaded.
* When the LoadedNetwork creates the backend-specific workloads for the layers, it creates a backend
  specific workload factory and calls this to create the workloads.

## The BackendRegistry

As mentioned above, all backends need to be registered through the BackendRegistry so ArmNN knows
about them. Registration requires a unique backend ID string and a lambda function that
returns a unique pointer to the [IBackendInternal interface](backendsCommon/IBackendInternal.hpp).

For registering a backend only this lambda function needs to exist, not the actual backend. This
allows dynamically creating the backend objects when they are needed.

The BackendRegistry has a few convenience functions, like we can query the registered backends and
 are able to tell if a given backend is registered or not.

## The ILayerSupport interface

ArmNN uses the [ILayerSupport](../../include/armnn/ILayerSupport.hpp) interface to decide if a layer
with a set of parameters (i.e. input and output tensors, descriptor, weights, filter, kernel if any) are
supported on a given backend. The backends need a way to communicate this information by implementing
the ```GetLayerSupport()``` function on the ```IBackendInternal``` interface.

Examples of this can be found in the [RefLayerSupport header](reference/RefLayerSupport.hpp)
and the [RefLayerSupport implementation](reference/RefLayerSupport.cpp).

## The IWorkloadFactory interface

The [IWorkloadFactory interface](backendsCommon/WorkloadFactory.hpp) is used for creating the backend
specific workloads. The factory function that creates the IWorkloadFactory object in the IBackendInterface
takes an IMemoryManager object.

To create a workload object the ```IWorkloadFactory``` takes a ```WorkloadInfo``` object that holds
the input and output tensor information and a workload specific queue descriptor.

## The IMemoryManager interface

Backends may choose to implement custom memory management. ArmNN supports this concept through the following
mechanism:

* the ```IBackendInternal``` interface has a ```CreateMemoryManager()``` function which is called before
  creating the workload factory
* the memory manager is passed to the ```CreateWorkloadFactory(...)``` function so the workload factory can
  use it for creating the backend-specific workloads
* the LoadedNetwork calls ```Acquire()``` on the memory manager before it starts executing the network and
  it calls ```Release()``` in its destructor

## The Optimizations

The backends may choose to implement backend-specific optimizations. This is supported through the ```GetOptimizations()```
method of the IBackendInternal interface. This function may return a vector of optimization objects and the optimizer
runs these after all general optimization is performed on the network.

## The IBackendContext interface

Backends may need to be notified whenever a network is loaded or unloaded. To support that, one can implement the optional
[IBackendContext](backendsCommon/IBackendContext.hpp) interface. The framework calls the ```CreateBackendContext(...)```
method for each backend in the Runtime. If the backend returns a valid unique pointer to a backend context, then the
runtime will hold this for its entire lifetime. It then calls the following interface functions for each stored context:

* ```BeforeLoadNetwork(NetworkId networkId)```
* ```AfterLoadNetwork(NetworkId networkId)```
* ```BeforeUnloadNetwork(NetworkId networkId)```
* ```AfterUnloadNetwork(NetworkId networkId)```