ArmNN
 22.02
src/backends/README.md
Go to the documentation of this file.
1 # Backend developer guide
2 
3 Arm NN allows adding new backends through the 'Pluggable Backend' mechanism.
4 
5 ## How to add a new backend
6 
7 Backends reside under [src/backends](./), in separate subfolders. For Linux builds they must have a ```backend.cmake``` file,
8 which is read automatically by [src/backends/backends.cmake](backends.cmake). The ```backend.cmake``` file
9 under the backend-specific folder is then included by the main CMakeLists.txt file at the root of the
10 Arm NN source tree.
11 
12 ### The backend.cmake file
13 
14 The ```backend.cmake``` has three main purposes:
15 
16 1. It makes sure the artifact (a cmake OBJECT library) is linked into the Arm NN shared library by appending the name of the library to the ```armnnLibraries``` list.
17 2. It makes sure that the subdirectory where backend sources reside gets included into the build.
18 3. To include backend-specific unit tests, the object library for the unit tests needs to be added to the ```armnnUnitTestLibraries``` list.
19 
20 Example ```backend.cmake``` file taken from [reference/backend.cmake](reference/backend.cmake):
21 
22 ```cmake
23 #
24 # Make sure the reference backend is included in the build.
25 # By adding the subdirectory, cmake requires the presence of CMakeLists.txt
26 # in the reference (backend) folder.
27 #
28 add_subdirectory(${PROJECT_SOURCE_DIR}/src/backends/reference)
29 
30 #
31 # Add the cmake OBJECT libraries built by the reference backend to the
32 # list of libraries linked against the Arm NN shared library.
33 #
34 list(APPEND armnnLibraries armnnRefBackend armnnRefBackendWorkloads)
35 
36 #
37 # Backend specific unit tests can be integrated through the
38 # armnnUnitTestLibraries variable. This makes sure that the
39 # UnitTests executable can run the backend-specific unit
40 # tests.
41 #
42 list(APPEND armnnUnitTestLibraries armnnRefBackendUnitTests)
43 ```
44 
45 ### The CMakeLists.txt file
46 
47 As described in the previous section, adding a new backend will require creating a ```CMakeLists.txt``` in
48 the backend folder. This follows the standard cmake conventions, and is required to build a static cmake OBJECT library
49 to be linked into the Arm NN shared library. As with any cmake build, the code can be structured into
50 subfolders and modules as the developer sees fit.
51 
52 Example can be found under [reference/CMakeLists.txt](reference/CMakeLists.txt).
53 
54 ### The backend.mk file
55 
56 Arm NN on Android uses the native Android build system. New backends are integrated by creating a
57 ```backend.mk``` file, which has a single variable called ```BACKEND_SOURCES``` listing all cpp
58 files to be built by the Android build system for the Arm NN shared library.
59 
60 Optionally, backend-specific unit tests can be added similarly, by
61 appending the list of cpp files to the ```BACKEND_TEST_SOURCES``` variable.
62 
63 Example taken from [reference/backend.mk](reference/backend.mk):
64 
65 ```make
66 BACKEND_SOURCES := \
67  RefLayerSupport.cpp \
68  RefWorkloadFactory.cpp \
69  workloads/Activation.cpp \
70  workloads/ElementwiseFunction.cpp \
71  workloads/Broadcast.cpp \
72  ...
73 
74 BACKEND_TEST_SOURCES := \
75  test/RefCreateWorkloadTests.cpp \
76  test/RefEndToEndTests.cpp \
77  test/RefJsonPrinterTests.cpp \
78  ...
79 ```
80 
81 ## How to add common code across backends
82 
83 For multiple backends that need common code, there is support for including them in the build
84 similarly to the backend code. This requires adding three files under a subfolder at the same level
85 as the backends folders. These are:
86 
87 1. common.cmake
88 2. common.mk
89 3. CMakeLists.txt
90 
91 They work the same way as the backend files. The only difference between them is that
92 common code is built first, so the backend code can depend on them.
93 
94 [aclCommon](aclCommon) is an example for this concept and you can find the corresponding files:
95 
96 1. [aclCommon/common.cmake](aclCommon/common.cmake)
97 2. [aclCommon/common.mk](aclCommon/common.mk)
98 3. [aclCommon/CMakeLists.txt](aclCommon/CMakeLists.txt)
99 
100 ## Identifying backends
101 
102 Backends are identified by a string that must be unique across backends. This string is
103 wrapped in the [BackendId](../../include/armnn/BackendId.hpp) object for backward compatibility
104 with previous Arm NN versions.
105 
106 ## The IBackendInternal interface
107 
108 All backends need to implement the [IBackendInternal](../../include/armnn/backends/IBackendInternal.hpp) interface.
109 The interface functions to be implemented are:
110 
111 ```c++
112  virtual IMemoryManagerUniquePtr CreateMemoryManager() const = 0;
113  virtual IWorkloadFactoryPtr CreateWorkloadFactory(
114  const IMemoryManagerSharedPtr& memoryManager = nullptr) const = 0;
115  virtual IBackendContextPtr CreateBackendContext(const IRuntime::CreationOptions&) const = 0;
116  virtual IBackendProfilingContextPtr CreateBackendProfilingContext(const IRuntime::CreationOptions& creationOptions,
117  armnn::profiling::IBackendProfiling& backendProfiling) const = 0;
118  virtual ILayerSupportSharedPtr GetLayerSupport() const = 0;
119  virtual Optimizations GetOptimizations() const = 0;
120  virtual SubgraphUniquePtr OptimizeSubgraph(const SubgraphView& subgraph, bool& optimizationAttempted) const;
121  virtual OptimizationViews OptimizeSubgraphView(const SubgraphView& subgraph) const;
122 ```
123 
124 Note that ```GetOptimizations()``` and ```SubgraphViewUniquePtr OptimizeSubgraphView(const SubgraphView& subgraph, bool& optimizationAttempted)```
125 have been deprecated.
126 The method ```OptimizationViews OptimizeSubgraph(const SubgraphView& subgraph)``` should be used instead to
127 apply specific optimizations to a given sub-graph.
128 
129 The Arm NN framework then creates instances of the IBackendInternal interface with the help of the
130 [BackendRegistry](../../include/armnn/BackendRegistry.hpp) singleton.
131 
132 **Important:** the ```IBackendInternal``` object is not guaranteed to have a longer lifetime than
133 the objects it creates. It is only intended to be a single entry point for the factory functions it has.
134 The best use of this is to be a lightweight, stateless object and make no assumptions between
135 its lifetime and the lifetime of the objects it creates.
136 
137 For each backend one needs to register a factory function that can
138 be retrieved using a [BackendId](../../include/armnn/BackendId.hpp).
139 The Arm NN framework creates the backend interfaces dynamically when
140 it sees fit and it keeps these objects for a short period of time. Examples:
141 
142 * During optimization Arm NN needs to decide which layers are supported by the backend.
143  To do this, it creates a backends and calls the ```GetLayerSupport()``` function and creates
144  an ```ILayerSupport``` object to help deciding this.
145 * During optimization Arm NN can run backend-specific optimizations. After splitting the graph into
146  sub-graphs based on backends, it calls the ```OptimizeSubgraphView()``` function on each of them and, if possible,
147  substitutes the corresponding sub-graph in the original graph with its optimized version.
148 * When the Runtime is initialized it creates an optional ```IBackendContext``` object and keeps this context alive
149  for the Runtime's lifetime. It notifies this context object before and after a network is loaded or unloaded.
150 * When the LoadedNetwork creates the backend-specific workloads for the layers, it creates a backend
151  specific workload factory and calls this to create the workloads.
152 
153 ## The BackendRegistry
154 
155 As mentioned above, all backends need to be registered through the BackendRegistry so Arm NN knows
156 about them. Registration requires a unique backend ID string and a lambda function that
157 returns a unique pointer to the [IBackendInternal interface](../../include/armnn/backends/IBackendInternal.hpp).
158 
159 For registering a backend only this lambda function needs to exist, not the actual backend. This
160 allows dynamically creating the backend objects when they are needed.
161 
162 The BackendRegistry has a few convenience functions, like we can query the registered backends and
163 are able to tell if a given backend is registered or not.
164 
165 Dynamic backends are registered during the runtime creation.
166 
167 ## The ILayerSupport interface
168 
169 Arm NN uses the [ILayerSupport](../../include/armnn/backends/ILayerSupport.hpp) interface to decide if a layer
170 with a set of parameters (i.e. input and output tensors, descriptor, weights, filter, kernel if any) are
171 supported on a given backend. The backends need a way to communicate this information by implementing
172 the ```GetLayerSupport()``` function on the ```IBackendInternal``` interface.
173 
174 Examples of this can be found in the [RefLayerSupport header](reference/RefLayerSupport.hpp)
175 and the [RefLayerSupport implementation](reference/RefLayerSupport.cpp).
176 
177 ## The IWorkloadFactory interface
178 
179 The [IWorkloadFactory interface](backendsCommon/WorkloadFactory.hpp) is used for creating the backend
180 specific workloads. The factory function that creates the IWorkloadFactory object in the IBackendInterface
181 takes an IMemoryManager object.
182 
183 To create a workload object the ```IWorkloadFactory``` takes a ```WorkloadInfo``` object that holds
184 the input and output tensor information and a workload specific queue descriptor.
185 
186 ## The IMemoryManager interface
187 
188 Backends may choose to implement custom memory management. Arm NN supports this concept through the following
189 mechanism:
190 
191 * the ```IBackendInternal``` interface has a ```CreateMemoryManager()``` function, which is called before
192  creating the workload factory
193 * the memory manager is passed to the ```CreateWorkloadFactory(...)``` function so the workload factory can
194  use it for creating the backend-specific workloads
195 * the LoadedNetwork calls ```Acquire()``` on the memory manager before it starts executing the network and
196  it calls ```Release()``` in its destructor
197 
198 ## The Optimizations
199 
200 The backends may choose to implement backend-specific optimizations.
201 This is supported through the method ```OptimizationViews OptimizeSubgraph(const SubgraphView& subgraph)``` of
202 the backend interface that allows the backends to apply their specific optimizations to a given sub-graph.
203 
204 The ```OptimizeSubgraph(...)``` method returns an OptimizationViews object containing three lists:
205 
206 * A list of the sub-graph substitutions: a "substitution" is a pair of sub-graphs, the first is the "substitutable" sub-graph,
207  representing the part of the original graph that has been optimized by the backend, while the second is the "replacement" sub-graph,
208  containing the actual optimized layers that will be replaced in the original graph correspondingly to the "substitutable" sub-graph
209 * A list of the failed sub-graphs: these are the parts of the original sub-graph that are not supported by the backend,
210  thus have been rejected. Arm NN will try to re-allocate these parts on other backends if available.
211 * A list of the untouched sub-graphs: these are the parts of the original sub-graph that have not been optimized,
212  but that can run (unoptimized) on the backend.
213 
214 The previous way backends had to provide a list optimizations to the Optimizer (through the ```GetOptimizations()``` method)
215 is still in place for backward compatibility, but it's now considered deprecated and will be remove in a future release.
216 
217 ## The IBackendContext interface
218 
219 Backends may need to be notified whenever a network is loaded or unloaded. To support that, one can implement the optional
220 [IBackendContext](../../include/armnn/backends/IBackendContext.hpp) interface. The framework calls the ```CreateBackendContext(...)```
221 method for each backend in the Runtime. If the backend returns a valid unique pointer to a backend context, then the
222 runtime will hold this for its entire lifetime. It then calls the following interface functions for each stored context:
223 
224 * ```BeforeLoadNetwork(NetworkId networkId)```
225 * ```AfterLoadNetwork(NetworkId networkId)```
226 * ```BeforeUnloadNetwork(NetworkId networkId)```
227 * ```AfterUnloadNetwork(NetworkId networkId)```
228 
229 ## The UseCustomMemoryAllocator interface
230 
231 Backends can also have an associated CustomMemoryAllocator registered with them that ArmNN will use to allocate
232 intra/inter-layer memory. This particular feature is required if you want a backend to use ProtectedContentAllocation.
233 To support this on your own backend you must implement the UseCustomMemoryAllocator interface.
234 
235 This interface returns a boolean value which indicates if the provided allocator is supported by
236 the backend. This interface is also used by the lambda function returned by the Backend Registry to configure
237 the CustomMemoryAllocator. Within the backend itself there should be a wrapper class to convert the generic
238 CustomMemoryAllocator provided by the interface into something that is more suitable for your own backend.
239 
240 Examples of how this can be done are in the [ClBackend header](cl/ClBackend.hpp) and the
241 [ClRegistryInitializer header](cl/ClRegistryInitializer.cpp)
242 
243 ## The GetCapabilities interface
244 
245 This is a list of BackendCapabilities currently supported by the backend. It consists of a constant list of
246 Name/Value pairs, each containing a string name, and a boolean value to indicate support. For example to
247 indicate support for ProtectedContentAllocation you would return {"ProtectedContentAllocation", true}
248 
249 An example can be found at the top of [ClBackend header](cl/ClBackend.hpp)
250 
251 ## Dynamic backends
252 
253 Backends can also be loaded by Arm NN dynamically at runtime.
254 To be properly loaded and used, the backend instances must comply to the standard interface for dynamic backends and to the versioning
255 rules that enforce ABI compatibility.
256 
257 ## Dynamic backends base interface
258 
259 The dynamic backend shared object must expose the following interface for Arm NN to handle it correctly:
260 
261 ```c++
262 extern "C"
263 {
264 const char* GetBackendId();
265 void GetVersion(uint32_t* outMajor, uint32_t* outMinor);
266 void* BackendFactory();
267 }
268 ```
269 
270 Interface details:
271 
272 * ```extern "C"``` is needed to use avoid C++ name mangling, necessary to allow Arm NN to dynamically load the symbols.
273 * ```GetBackendId()```: must return the unique id of the dynamic backends.
274  If at the time of the loading the id already exists in the internal Arm NN's backend registry, the backend will be skipped and
275  not loaded in Arm NN
276 * ```GetVersion()```: must return the version of the dynamic backend.
277  The version must indicate the version of the Backend API the dynamic backend has been built with.
278  The current Backend API version can be found by inspecting the IBackendInternal interface.
279  At the time of loading, the version of the backend will be checked against the version of the Backend API Arm NN is built with.
280  If the backend version is not compatible with the current Backend API, the backend will not be loaded as it will be assumed that
281  it is not ABI compatible with the current Arm NN build.
282 * ```BackendFactory()```: must return a valid instance of the backend.
283  The backend instance is an object that must inherit from the version of the IBackendInternal interface declared by GetVersion().
284  It is the backend developer's responsibility to ensure that the backend implementation correctly reflects the version declared by
285  GetVersion(), and that the object returned by the BackendFactory() function is a valid and well-formed instance of the IBackendInternal
286  interface.
287 
288 ## Dynamic backend versioning and ABI compatibility
289 
290 Dynamic backend versioning policy:
291 
292 Updates to Arm NN's Backend API follow these rules: changes to the Backend API (the IBackendInternal interface) that break
293 ABI compatibility with the previous API version will be indicated by a change of the API's major version, while changes
294 that guarantee ABI compatibility with the previous API version will be indicated by a change in API's the minor version.
295 
296 For example:
297 
298 * Dynamic backend version 2.4 (i.e. built with Backend API version 2.4) is compatible with Arm NN's Backend API version 2.4
299  (same version, backend built against the same Backend API)
300 * Dynamic backend version 2.1 (i.e. built with Backend API version 2.1) is compatible with Arm NN's Backend API version 2.4
301  (same major version, backend built against earlier compatible API)
302 * Dynamic backend version 2.5 (i.e. built with Backend API version 2.5) is not compatible with Arm NN's Backend API version 2.4
303  (same major version, backend built against later incompatible API, backend might require update to the latest compatible backend API)
304 * Dynamic backend version 2.0 (i.e. built with Backend API version 2.0) is not compatible with Arm NN's Backend API version 1.0
305  (backend requires a completely new API version)
306 * Dynamic backend version 2.0 (i.e. built with Backend API version 2.0) is not compatible with Arm NN's Backend API version 3.0
307  (backward compatibility in the Backend API is broken)
308 
309 ## Dynamic backend loading paths
310 
311 During the creation of the Runtime, Arm NN will scan a given set of paths searching for suitable dynamic backend objects to load.
312 A list of (absolute) paths can be specified at compile-time by setting a define named ```DYNAMIC_BACKEND_PATHS``` in the form of a colon-separated list of strings.
313 
314 ```shell
315 -DDYNAMIC_BACKEND_PATHS="PATH_1:PATH_2...:PATH_N"
316 ```
317 
318 The paths will be processed in the same order as they are indicated in the macro.
319 
320 It is also possible to override those paths at runtime when creating the Runtime object by setting the value of the ```m_DynamicBackendsPath``` member in the CreationOptions class.
321 Only one path is allowed for the override via the CreationOptions class.
322 By setting the value of the ```m_DynamicBackendsPath``` to a path in the filesystem, Arm NN will entirely ignore the list of paths passed via the
323 ```DYNAMIC_BACKEND_PATHS``` compiler directive.
324 
325 All the specified paths are validated before processing (they must exist, must be directories, and must be absolute paths),
326 in case of error a warning message will be added to the log, but Arm NN's execution will not be stopped.
327 If all paths are not valid, then no dynamic backends will be loaded in the Arm NN's runtime.
328 
329 Passing an empty list of paths at compile-time and providing no path override at runtime will effectively disable the
330 dynamic backend loading feature, and no dynamic backends will be loaded into Arm NN's runtime.
331 
332 ## Dynamic backend file naming convention
333 
334 During the creation of a Runtime object, Arm NN will scan the paths specified for dynamic backend loading searching for suitable backend objects.
335 Arm NN will try to load only the files that match the following accepted naming scheme:
336 
337 ```shell
338 <vendor>_<name>_backend.so[<version>] (e.g. "Arm_GpuAcc_backend.so" or "Arm_GpuAcc_backend.so.1.2.3")
339 ```
340 
341 Only alphanumeric characters are allowed for both the `<vendor>` and the `<name>` fields, namely lowercase and/or uppercase characters,
342 and/or numerical digits (see the table below for examples).
343 Only dots and numbers are allowed for the optional `<version>` field.
344 
345 Symlinks to other files are allowed to support the standard linux shared object versioning:
346 
347 ```shell
348 Arm_GpuAcc_backend.so -> Arm_GpuAcc_backend.so.1.2.3
349 Arm_GpuAcc_backend.so.1 -> Arm_GpuAcc_backend.so.1.2.3
350 Arm_GpuAcc_backend.so.1.2 -> Arm_GpuAcc_backend.so.1.2.3
351 Arm_GpuAcc_backend.so.1.2.3
352 ```
353 
354 Files are identified by their full canonical path, so it is allowed to have files with the same name in different directories.
355 However, if those are actually the same dynamic backend, only the first in order of parsing will be loaded.
356 
357 Examples:
358 
359 | Filename | Description |
360 | -------------------------------------------------------- | ------------------------------------------------- |
361 | Arm_GpuAcc_backend.so | valid: basic backend name |
362 | Arm_GpuAcc_backend.so.1 | valid: single field version number |
363 | Arm_GpuAcc_backend.so.1.2 | valid: multiple field version number |
364 | Arm_GpuAcc_backend.so.1.2.3 | valid: multiple field version number |
365 | Arm_GpuAcc_backend.so.10.1.27 | valid: Multiple digit version |
366 | Arm_GpuAcc_backend.so.10.1.33. | not valid: dot not followed by version number |
367 | Arm_GpuAcc_backend.so.3.4..5 | not valid: dot not followed by version number |
368 | Arm_GpuAcc_backend.so.1,1.1 | not valid: comma instead of dot in the version |
369 | Arm123_GpuAcc_backend.so | valid: digits in vendor name are allowed |
370 | Arm_GpuAcc456_backend.so | valid: digits in backend id are allowed |
371 | Arm%Co_GpuAcc_backend.so | not valid: invalid character in vendor name |
372 | Arm_Gpu.Acc_backend.so | not valid: invalid character in backend id |
373 | GpuAcc_backend.so | not valid: missing vendor name |
374 | _GpuAcc_backend.so | not valid: missing vendor name |
375 | Arm__backend.so | not valid: missing backend id |
376 | Arm_GpuAcc.so | not valid: missing "backend" at the end |
377 | __backend.so | not valid: missing vendor name and backend id |
378 | __.so | not valid: missing all fields |
379 | Arm_GpuAcc_backend | not valid: missing at least ".so" at the end |
380 | Arm_GpuAcc_backend_v1.2.so | not valid: extra version info at the end |
381 | Arm_CpuAcc_backend.so | valid: basic backend name |
382 | Arm_CpuAcc_backend.so.1 -> Arm_CpuAcc_backend.so | valid: symlink to valid backend file |
383 | Arm_CpuAcc_backend.so.1.2 -> Arm_CpuAcc_backend.so.1 | valid: symlink to valid symlink |
384 | Arm_CpuAcc_backend.so.1.2.3 -> Arm_CpuAcc_backend.so.1.2 | valid: symlink to valid symlink |
385 | Arm_no_backend.so -> nothing | not valid: symlink resolves to non-existent file |
386 | pathA/Arm_GpuAcc_backend.so | valid: basic backend name |
387 | pathB/Arm_GpuAcc_backend.so | valid: but duplicated from pathA/ |
388 
389 Arm NN will try to load the dynamic backends in the same order as they are parsed from the filesystem.
390 
391 ## Dynamic backend examples
392 
393 The source code includes an example that is used to generate some mock dynamic backends for testing purposes. The source files are:
394 
395 [TestDynamicBackend.hpp](backendsCommon/test/TestDynamicBackend.hpp)
396 [TestDynamicBackend.cpp](backendsCommon/test/TestDynamicBackend.cpp)
397 
398 This example is useful for going through all the use cases that constitute an invalid dynamic backend object, such as
399 an invalid/malformed implementation of the shared object interface, or an invalid value returned by any of the interface methods
400 that would prevent Arm NN from making use of the dynamic backend.
401 
402 A dynamic implementation of the reference backend is also provided. The source files are:
403 
404 [RefDynamicBackend.hpp](dynamic/reference/RefDynamicBackend.hpp)
405 [RefDynamicBackend.cpp](dynamic/reference/RefDynamicBackend.cpp)
406 
407 The implementation itself is quite simple and straightforward. Since an implementation of this particular backend was already available,
408 the dynamic version is just a wrapper around the original code that simply returns the backend id, version and an instance of the
409 backend itself via the factory function.
410 For the sake of the example, the source code of the reference backend is used to build the dynamic version (as you would for any new
411 dynamic backend), while all the other symbols needed are provided by linking the dynamic backend against Arm NN.
412 
413 The makefile used for building the reference dynamic backend is also provided: [CMakeLists.txt](dynamic/reference/CMakeLists.txt)
414 
415 A unit test that loads the reference backend dynamically and that exercises it is also included in the file
416 [DynamicBackendTests.cpp](dynamic/backendsCommon/test/DynamicBackendTests.cpp), by the test case ```CreateReferenceDynamicBackend```.
417 In the test, a path on the filesystem is scanned for valid dynamic backend files (using the override option in ```CreationOptions```)
418 where only the reference dynamic backend is.
419 In this example the file is named ```Arm_CpuRef_backend.so```, which is compliant with the expected naming scheme for dynamic backends.
420 A ```DynamicBackend``` is created in the runtime to represent the newly loaded backend, then the backend is registered in the Backend
421 Registry with the id "CpuRef" (returned by ```GetBackendId()```).
422 The unit test makes sure that the backend is actually registered in Arm NN, before trying to create an instance of the backend by
423 calling the factory function provided through the shared object interface (```BackendFactory()```).
424 The backend instance is used to verify that everything is in order, testing basic 2D convolution support by making use of the
425 Layer Support API and the Workload Factory.
426 At the end of test, the runtime object goes out of scope and the dynamic backend instance is automatically destroyed, and the handle to
427 the shared object is closed.