blob: b9bc7e09ce4c043fbec737cb8b6ec13e2da586a6 [file] [log] [blame]
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +00001///
ramelg0136a1c112022-03-29 19:09:56 +01002/// Copyright (c) 2018-2022 Arm Limited.
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +00003///
4/// SPDX-License-Identifier: MIT
5///
6/// Permission is hereby granted, free of charge, to any person obtaining a copy
7/// of this software and associated documentation files (the "Software"), to
8/// deal in the Software without restriction, including without limitation the
9/// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
10/// sell copies of the Software, and to permit persons to whom the Software is
11/// furnished to do so, subject to the following conditions:
12///
13/// The above copyright notice and this permission notice shall be included in all
14/// copies or substantial portions of the Software.
15///
16/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22/// SOFTWARE.
23///
24
25namespace arm_compute
26{
27/**
Sheri Zhangd813bab2021-04-30 16:53:41 +010028@page adding_operator How to Add a New Operator
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000029
30@tableofcontents
31
Sheri Zhangd813bab2021-04-30 16:53:41 +010032@section S4_0_introduction Adding new operators
33
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000034@section S4_1_introduction Introduction
Michele Di Giorgio57f30a92020-09-08 14:03:51 +010035In Compute Library there are two main parts or modules:
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000036- The core library consists of a low-level collection of algorithms implemented in C++ and optimized for Arm CPUs and GPUs. The core module is designed to be embedded in other projects and it doesn't perform any memory management or scheduling.
37- The runtime library is a wrapper of the core library and provides other additional features like memory management, multithreaded execution of workloads and allocation of the intermediate tensors.
38
39The library can be integrated in an existing external library or application that provides its own scheduler or a specific memory manager. In that case, the right solution is to use only the core library which means that the user must also manage all the memory allocation not only for the input/output tensor but also for the intermediate tensors/variables necessary. On the other hand, if the user doesn't want to care about allocation and multithreading then the right choice is to use the functions from the runtime library.
40
41Apart from these components that get linked into the application, the sources also include the validation test suite and the C++ reference implementations against which all the operators are validated.
42
43
44@section S4_1_supporting_new_operators Supporting new operators
45
Michele Di Giorgio57f30a92020-09-08 14:03:51 +010046Following are the steps involved in adding support for a new operator in Compute Library
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000047- Add new data types (if required)
48- Add the kernel to the core library.
49- Add the function to the runtime library.
50- Add validation tests.
51 - Add the reference implementation.
52 - Add the fixture
53 - register the tests.
54
55@subsection S4_1_1_add_datatypes Adding new data types
56
Michele Di Giorgio57f30a92020-09-08 14:03:51 +010057Compute Library declares a few new datatypes related to its domain, kernels, and functions in the library process Tensors and Images (Computer Vision functions). Tensors are multi-dimensional arrays with a maximum of Coordinates::num_max_dimensions dimensions; depending on the number of dimensions tensors can be interpreted as various objects. A scalar can be represented as a zero-dimensional tensor and a vector of numbers can be represented as a one-dimensional tensor. Furthermore, an image is just a 2D tensor, a 3D tensor can be seen as an array of images and a 4D tensor as a 2D array of images, etc.
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000058All the datatype classes or structures are grouped in the core library folder arm_compute/core like the @ref ITensor, @ref ITensorInfo (all the information of a tensor), TensorShape and simpler types are in arm_compute/core/Types.h.
59
60If an operator handles a new datatype, it must be added to the library. While adding a new data type to the library, it's necessary to implement the function to enable printing, the to_string() method and the output stream insertion (<<) operator. Every datatype implements these two functions in utils/TypePrinter.h
61
ramelg0136a1c112022-03-29 19:09:56 +010062A quick example, in <a href="https://github.com/ARM-software/ComputeLibrary/blob/main/arm_compute/core/Types.h">Types.h</a> we add:
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000063
64@snippet arm_compute/core/Types.h DataLayout enum definition
65
66And for printing:
67
68@snippet utils/TypePrinter.h Print DataLayout type
69
Michele Di Giorgio57f30a92020-09-08 14:03:51 +010070In Compute Library, we use namespaces to group all the operators, functions, classes and interfaces. The main namespace to use is arm_compute. In the test suite, the test framework and the individual tests use nested namespaces like @ref test::validation or @ref test::benchmark to group the different purposes of various parts of the suite.
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000071Utility functions like conversion or type cast operators, that are shared by multiple operators are in arm_compute/core/Utils.h. Non-inlined function definitions go in the corresponding .cpp files in the src folder.
72Similarly, all common functions that process shapes, like calculating output shapes of an operator or shape conversions etc are in arm_compute/core/utils/misc/ShapeCalculator.h.
73
74
75@subsection S4_1_2_add_kernel Add a kernel
Michele Di Giorgio33f41fa2021-03-09 14:09:08 +000076As we mentioned at the beginning, the kernel is the implementation of the operator or algorithm partially using a specific programming language related to the backend we want to use. Adding a kernel in the library means implementing the algorithm in a SIMD technology like Arm® Neon™ or OpenCL. All kernels in Compute Library must implement a common interface IKernel or one of the specific subinterfaces.
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000077IKernel is the common interface for all the kernels in the core library, it contains the main methods for configure and run the kernel itself, such as window() that return the maximum window the kernel can be executed on or is_parallelisable() for indicate whether or not the kernel is parallelizable. If the kernel is parallelizable then the window returned by the window() method can be split into sub-windows which can then be run in parallel, in the other case, only the window returned by window() can be passed to the run method.
Jakub Sujakee301b32021-06-04 09:46:08 +010078There are specific interfaces for OpenCL and Neon™: @ref ICLKernel, INEKernel (using INEKernel = @ref ICPPKernel).
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000079
80- @ref ICLKernel is the common interface for all the OpenCL kernels. It implements the inherited methods and adds all the methods necessary to configure the CL kernel, such as set/return the Local-Workgroup-Size hint, add single, array or tensor argument, set the targeted GPU architecture according to the CL device. All these methods are used during the configuration and the run of the operator.
Jakub Sujakee301b32021-06-04 09:46:08 +010081- INEKernel inherits from @ref IKernel as well and it's the common interface for all kernels implemented in Neon™, it adds just the run and the name methods.
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000082
83There are two others implementation of @ref IKernel called @ref ICLSimpleKernel and INESimpleKernel, they are the interface for simple kernels that have just one input tensor and one output tensor.
84Creating a new kernel implies adding new files:
Sang-Hoon Parkbef7fa22020-10-21 15:58:54 +010085- src/core/CL/kernels/CLReshapeLayerKernel.h
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000086- src/core/CL/cl_kernels/reshape_layer.cl
87- src/core/CL/kernels/CLReshapeLayerKernel.cpp
88- src/core/CL/CLKernelLibrary.cpp
89
Jakub Sujakee301b32021-06-04 09:46:08 +010090Neon™ kernel
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000091- arm_compute/core/NEON/kernels/NEReshapeLayerKernel.h
92- src/core/NEON/kernels/NEReshapeLayerKernel.cpp
93
94We must register the new layer in the respective libraries:
Sang-Hoon Parkbef7fa22020-10-21 15:58:54 +010095- src/core/CL/CLKernels.h
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000096- arm_compute/core/NEON/NEKernels.h
97
Michele Di Giorgio57f30a92020-09-08 14:03:51 +010098These files contain the list of all kernels available in the corresponding Compute Library's backend, for example CLKernels:
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +000099@code{.cpp}
ramelg0136a1c112022-03-29 19:09:56 +0100100...
Sang-Hoon Parkbef7fa22020-10-21 15:58:54 +0100101#include "src/core/CL/kernels/CLMinMaxLayerKernel.h"
102#include "src/core/CL/kernels/CLMinMaxLocationKernel.h"
ramelg0136a1c112022-03-29 19:09:56 +0100103...
Sang-Hoon Parkbef7fa22020-10-21 15:58:54 +0100104#include "src/core/CL/kernels/CLReshapeLayerKernel.h"
ramelg0136a1c112022-03-29 19:09:56 +0100105...
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +0000106
107@endcode
108
109For OpenCL we need to update the CLKernelLibrary.cpp, adding the appropriate code to embed the .cl kernel in the library. The OpenCL code can be compiled offline and embed in the library as binary.
110The essential operation we want to do with a kernel will be
111- create the kernel object
112- initialize the kernel with the input/output and any other parameters that may be required
113- retrieve the execution window of the kernel and run the whole kernel window in the current thread or use the multithreading.
114
115Each kernel will have to implement the method:
116- %validate: is a static function that checks if the given info will lead to a valid configuration of the kernel.
117- configure: configure the kernel, its window, accessor, valid region, etc for the given set of tensors and other parameters.
118- run: execute the kernel in the window
119
120The structure of the kernel .cpp file should be similar to the next ones.
121For OpenCL:
Georgios Pinitas7891a732021-08-20 21:39:25 +0100122@snippet src/gpu/cl/kernels/ClReshapeKernel.cpp ClReshapeKernel Kernel
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +0000123The run will call the function defined in the .cl file.
124
Michele Di Giorgio33f41fa2021-03-09 14:09:08 +0000125For the Arm® Neon™ backend case:
Georgios Pinitas7891a732021-08-20 21:39:25 +0100126@snippet src/cpu/kernels/CpuReshapeKernel.cpp NEReshapeLayerKernel Kernel
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +0000127
Michele Di Giorgio33f41fa2021-03-09 14:09:08 +0000128In the Arm® Neon™ case, there is no need to add an extra file and we implement the kernel in the same NEReshapeLayerKernel.cpp file.
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +0000129If the tests are already in place, the new kernel can be tested using the existing tests by adding the configure and run of the kernel to the compute_target() in the fixture.
130
131
132@subsection S4_1_3_add_function Add a function
133
134%Memory management and scheduling the underlying kernel(s) must be handled by the function implementation. A kernel class must support window() API which return the execute window for the configuration that the kernel is configured for. A window specifies the dimensions of a workload. It has a start and end on each of the dimension. A maximum of Coordinates::num_max_dimensions is supported. The run time layer is expected to query the kernel for the window size and schedule the window as it sees fit. It could choose to split the window into sub windows so that it could be run in parallel. The split must adhere to the following rules
135
136- max[n].start() <= sub[n].start() < max[n].end()
137- sub[n].start() < sub[n].end() <= max[n].end()
138- max[n].step() == sub[n].step()
139- (sub[n].start() - max[n].start()) % max[n].step() == 0
140- (sub[n].end() - sub[n].start()) % max[n].step() == 0
141
Michele Di Giorgio33f41fa2021-03-09 14:09:08 +0000142@ref CPPScheduler::schedule provides a sample implementation that is used for Arm® Neon™ kernels.
143%Memory management is the other aspect that the runtime layer is supposed to handle. %Memory management of the tensors is abstracted using TensorAllocator. Each tensor holds a pointer to a TensorAllocator object, which is used to allocate and free the memory at runtime. The implementation that is currently supported in Compute Library allows memory blocks, required to be fulfilled for a given operator, to be grouped together under a @ref MemoryGroup. Each group can be acquired and released. The underlying implementation of memory groups vary depending on whether Arm® Neon™ or CL is used. The memory group class uses memory pool to provide the required memory. It also uses the memory manager to manage the lifetime and a IPoolManager to manage the memory pools registered with the memory manager.
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +0000144
145
146We have seen the various interfaces for a kernel in the core library, the same structure the same file structure design exists in the runtime module. IFunction is the base class for all the functions, it has two child interfaces: ICLSimpleFunction and INESimpleFunction that are used as base class for functions which call a single kernel.
147
Michele Di Giorgio33f41fa2021-03-09 14:09:08 +0000148The new operator has to implement %validate(), configure() and run(), these methods will call the respective function in the kernel considering that the multi-threading is used for the kernels which are parallelizable, by default std::thread::hardware_concurrency() threads are used. For Arm® Neon™ function can be used CPPScheduler::set_num_threads() to manually set the number of threads, whereas for OpenCL kernels all the kernels are enqueued on the queue associated with CLScheduler and the queue is then flushed.
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +0000149For the runtime functions, there is an extra method implemented: prepare(), this method prepares the function for the run, it does all the heavy operations that are done only once (reshape the weight, release the memory not necessary after the reshape, etc). The prepare method can be called standalone or in the first run, if not called before, after then the function will be marked as prepared.
150The files we add are:
151
152OpenCL function
153- arm_compute/runtime/CL/functions/CLReshapeLayer.h
154- src/runtime/CL/functions/CLReshapeLayer.cpp
155
Jakub Sujakee301b32021-06-04 09:46:08 +0100156Neon™ function
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +0000157- arm_compute/runtime/NEON/functions/NEReshapeLayer.h
158- src/runtime/NEON/functions/NEReshapeLayer.cpp
159
160As we did in the kernel we have to edit the runtime libraries to register the new operator modifying the relative library file:
161- arm_compute/runtime/CL/CLFunctions.h
162- arm_compute/runtime/NEON/NEFunctions.h
163
164For the special case where the new function calls only one kernel, we could use as base class ICLSimpleFunction or INESimpleFunction. The configure and the validate methods will simply call the corresponding functions. The structure will be:
165@snippet src/runtime/CL/functions/CLReshapeLayer.cpp CLReshapeLayer snippet
166
167
168If the function is more complicated and calls more than one kernel we have to use the memory manager to manage the intermediate tensors; in the configure() method we call the manage() function passing the tensor to keep track, in the run method we will have to acquire all the buffer managed and released at the end.
169For OpenCL if we want to add two tensor input and reshape the result:
170
171@code{.cpp}
172using namespace arm_compute;
173
174CLAddReshapeLayer:: CLAddReshapeLayer(std::shared_ptr<IMemoryManager> memory_manager)
175 : _memory_group(std::move(memory_manager))
176{
177}
178
179void CLAddReshapeLayer::configure(const ICLTensor *input1, const ICLTensor *input2, ICLTensor *output)
180{
181 // Allocate memory
182 TensorInfo info();
183 add_output.allocator()->init(info);
184
185 // Manage intermediate buffers
186 memory_group.manage(&_addOutput);
187
188 // Initialise kernel
189 _add_kernel.configure(input1, input2, &add_output);
190 _reshape_kernel.configure(&add_output, output);
191
192 // Allocate intermediate tensors
193 add_output.allocator()->allocate();
194}
195
196Status CLAddReshapeLayer::validate(const ITensorInfo *input1, const ITensorInfo *input2, const ITensorInfo *output)
197{
198 TensorInfo add_output();
199 ARM_COMPUTE_RETURN_ERROR_ON(CLAddLayerKernel::validate(input1, input2, add_output));
200 ARM_COMPUTE_RETURN_ERROR_ON(CLReshapeLayerKernel::validate(add_output, output));
201 return Status{};
202}
203
204void CLAddReshapeLayer::run()
205{
206 memory_group.acquire();
207
208 // Run Add
209 add_kernel.run();
210
211 // Run Reshape
212 CLScheduler::get().enqueue(reshape_kernel);
213
214 memory_group.release();
215}
216
217@endcode
218
Jakub Sujakee301b32021-06-04 09:46:08 +0100219For Neon™:
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +0000220
221@code{.cpp}
222using namespace arm_compute;
223
224NEAddReshapeLayer:: NEAddReshapeLayer (std::shared_ptr<IMemoryManager> memory_manager)
225 : _memory_group(std::move(memory_manager))
226{
227}
228
229void NEAddReshapeLayer::configure(const ITensor *input1, const ITensor *input2, ITensor *output)
230{
231 // Allocate memory
232 TensorInfo info();
233 add_output.allocator()->init(info);
234
235 // Manage intermediate buffers
236 memory_group.manage(&_addOutput);
237
238 // Initialise kernel
239 add_kernel.configure(input1, input2, &addOutput);
240 reshape_kernel.configure(&addOutput, output);
241
242 // Allocate intermediate tensors
243 add_output.allocator()->allocate();
244}
245
246void NEAddReshapeLayer::run()
247{
248 memory_group.acquire();
249
250 // Run Add
251 add_kernel.run();
252
253 // Run Reshape
254 NEScheduler::get().schedule(_reshape_kernel.get(), Window::DimY);
255
256 memory_group.release();
257}
258@endcode
259
260
261At this point, everything is in place at the library level. If you are following an tests driven implementation and all the tests are already in place, we can call the function configuration in the fixture and remove any redundant code like the allocation of the intermediate tensors since it's done in the function. Run the final tests to check the results match with the expected results from the reference implementation.
262
263@subsection S4_1_4_add_validation Add validation artifacts
264
265@subsubsection S4_1_4_1_add_reference Add the reference implementation and the tests
266As mentioned in the introduction, the reference implementation is a pure C++ implementation without any optimization or backend specific instruction.
Jakub Sujakee301b32021-06-04 09:46:08 +0100267The reference implementation consist of two files into the folder tests/validation/reference:
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +0000268- tests/validation/reference/ReshapeLayer.h
269- tests/validation/reference/ReshapeLayer.cpp
270
271where we will put respectively the declaration and definition of the new operator.
272All the utility functions that are used ONLY in the tests are in test/validation/helpers.h, for all the others, as mentioned before, there are helpers in the library.
Michele Di Giorgio57f30a92020-09-08 14:03:51 +0100273Compute Library and the tests do use templates, the reference implementation is a generic implementation independent from the datatype and we use the templates to generalize the datatype concept.
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +0000274Following the example, let's have a look at the ReshapeLayer operator:
275
276- tests/validation/reference/ReshapeLayer.h
277
278@snippet tests/validation/reference/ReshapeLayer.h ReshapeLayer
279
280- tests/validation/reference/ReshapeLayer.cpp
281
282@snippet tests/validation/reference/ReshapeLayer.cpp ReshapeLayer
283
284An explicit instantiation of the template for the required datatypes must be added in the .cpp file.
285
286@subsubsection S4_1_4_2_add_dataset Add dataset
287One of the parameters of the tests is the dataset, it will be used to generate versions of the test case with different inputs.
288To pass the dataset at the fixture data test case we have three cases
289- the operator dataset is simple so it can be added directly in the test case data declaration
290- we can create a class that return tuples at the test framework
291
292@snippet tests/datasets/PoolingTypesDataset.h PoolingTypes datasets
293
294- if we want to create dynamically the dataset combining different parameter, we can create the dataset using iterators.
295For example, dataset for ReshapeLayer:
296
297@snippet tests/datasets/ReshapeLayerDataset.h ReshapeLayer datasets
298
299@subsubsection S4_1_4_3_add_fixture Add a fixture and a data test case
300
301Benchmark and validation tests are based on the same framework to setup and run the tests. In addition to running simple, self-contained test functions the framework supports fixtures and data test cases.
302Fixtures can be used to share common setup, teardown or even run tasks among multiple test cases, for that purpose a fixture can define a "setup", "teardown" and "run" method.
Jakub Sujakee301b32021-06-04 09:46:08 +0100303Adding tests for the new operator in the runtime library we need to implement at least the setup method, that is used to call two methods for configure, run and return the output respectively of the target (CL or Neon™) and the reference (C++ implementation).
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +0000304
305For example let's have a look at Reshape Layer Fixture :
306
307@snippet tests/validation/fixtures/ReshapeLayerFixture.h ReshapeLayer fixture
308
309In the fixture class above we can see that the setup method computes the target and reference and store them in the two members _target and _reference which will be used later to check for correctness.
310The compute_target method reflects the exact behavior expected when we call a function. The input and output tensor must be declared, function configured, tensors allocated, the input tensor filled with required data, and finally, the function must be run and the results returned.
311This fixture is used in the test case, that is a parameterized test case that inherits from a fixture. The test case will have access to all public and protected members of the fixture. Only the setup and teardown methods of the fixture will be used. The setup method of the fixture needs to be a template and must accept inputs from the dataset as arguments.
312The body of this function will be used as a test function.
Jakub Sujakee301b32021-06-04 09:46:08 +0100313For the fixture test case the first argument is the name of the test case (has to be unique within the enclosing test suite), the second argument is the class name of the fixture, the third argument is the dataset mode in which the test will be active (PRECOMMIT or NIGHTLY) and the fourth argument is the dataset.
Vidhya Sudhan Loganathand646ae12018-11-19 15:18:20 +0000314For example:
315
316@snippet tests/validation/CL/ActivationLayer.cpp CLActivationLayerFixture snippet
317
318@code{.cpp}
319TEST_SUITE(CL)
320TEST_SUITE(ActivationLayer)
321TEST_SUITE(Float)
322TEST_SUITE(FP16)
323@endcode
324@snippet tests/validation/CL/ActivationLayer.cpp CLActivationLayer Test snippet
325@code{.cpp}
326TEST_SUITE_END()
327TEST_SUITE_END()
328TEST_SUITE_END()
329TEST_SUITE_END()
330@endcode
331
332This will produce a set of tests that can be filtered with "CL/ReshapeLayer/Float/FP16/RunSmall". Each test produced from the cartesian product of the dataset is associated to a number and can be filtered specifying all the parameters.
333*/
334} // namespace arm_compute