This document demonstrates how to add new functions
(also called as layer functions
) to NNabla.
The following instruction demonstrates the new layer addition procedure for Ubuntu 16.04 platform.
However instructions for macOS and Windows platforms are also same except NNabla building part.
Download the Neural Network Libraries from https://github.com/sony/nnabla
and build from source.
For detailed instructions on building NNabla from source please see Build Manuals.
Following are the steps to be followed to add new layer function.
- Write a definition in YAML.
- Write function definition in
functions.yaml
. - Write type specification of the new function in
function_types.yaml
.
- Write function definition in
- Generate new function code.
- Implement new function logic.
- Write unit test cases for new function.
- Write benchmark unit test cases for new function (optional).
- Write function documentation.
First step for adding new layer function is to define the function definition in functions.yaml.
Following is the typical syntax for function definition in functions.yaml
.
class_name_of_new_function:
snake_name: name_of_new_function
doc: |2
Write description for the functionality of new layer function.
inputs:
input_1:
doc: Describe the input parameters.
optional: boolean value indicating if input is optional.
parameter: boolean value indicating if input is parameter.
input_2:
.
.
.
input_n:
arguments:
arg_1:
doc: Describe the argument.
type: Describe type of the argument.
default: default value of the argument
arg_2:
.
.
.
arg_n:
outputs:
y:
doc: Describe the output parameter.
Following is example function definition of sum
function in nnabla.
Sum:
snake_name: sum
doc: |2
Reduces a matrix along a specified axis with the sum function.
inputs:
x:
doc: N-D array.
arguments:
axes:
doc: Axes to be reduced. If empty list is given, all dimensions are reduced
to scalar.
type: repeated int64
default: range(x.ndim)
keep_dims:
doc: Flag whether the reduced axis is kept.
type: bool
default: 'False'
outputs:
y:
doc: N-D array
Note:
Please ignore function_ids
. This entry is the function id and its history used by internal code generation, and it is updated when compile NNabla.
After adding the function definition, next step is to add a type specification of the new function to function_types.yaml.
A function class generated by the code generator and most of the existing functions are written as a template class, in which storage types are defined as template argument(s) of the class.
Using the storage type defined in function_types.yaml
file, the build system will generate a source file in which template instantiation with specified types are written.
Also, the typed functions are registered to a function factory so that a typed function can be queried with a type configuration name.
NNabla supports two storage types, which will be used internally to cast input and output data while executing.
Following are the storage types currently supported in nnabla.
- float: Represents any basic data types. E.g. float, int.
- half: Represents any basic data types (E.g. int) or 16-bit floating point internal data type Half.
We can set the storage type while setting the context as follows.
# Get context.
from nnabla.ext_utils import get_extension_context
# for 32-bit floating point data type, set type_config='float'
ctx = get_extension_context(
args.context, device_id=args.device_id, type_config='float')
or
# for 16-bit floating point data type, set type_config='half'
ctx = get_extension_context(
args.context, device_id=args.device_id, type_config='half')
nn.set_default_context(ctx)
Following is the syntax of function_types.yaml
.
class_name_of_new_function:
storage_type_name: [data_type_1, data_type_2 .. data_type_n ]
storage_type_name: [data_type_1, data_type_2 .. data_type_n ]
Following is example for type specification of Embed
function in nnabla.
Embed:
float: [int, float]
half: [int, Half]
After editing the .yaml
files, next step is to generate the template class for new function.
By running cmake as described in the build instruction (Please refer Build Manuals.),
our build system will create the following files using the function definition.
cd build
cmake ..
-
Function class template (created only if they don't exist, and should be added to the Git version control)
include/nbla/function/${snake_name}.hpp
src/nbla/function/generic/${snake_name}.cpp
python/src/nnabla/backward_function/${snake_name}.py
-
Template type instantiation of functions (overwritten)
src/nbla/function/${snake_name}.cpp
-
Initialization, function registration to factories (overwritten)
src/nbla/init.cpp
-
Python interfaces at
python/src/nnabla
(overwritten)_version.py
function.pxd
function.pyx
function_bases.py
-
Serializer/Deserializer (overwritten)
python/src/nnabla/utils/load_function.py
python/src/nnabla/utils/save_function.py
src/nbla_utils/nnp_impl_create_function.cpp
Although the function template are useful, element-wise operation functions such as an activation function can be written by using macro functions.
Various macros are provided for easily defining functions with common structures. Please look inside existing functions for examples of these macros.
If you are adding new layer function which does element-wise operations, then you need not implement the generated template class.
Instead you can use the element-wise macro provided by nnabla.
Following are the steps to implement the element-wise operation based new function.
- In the ${snake_name}.cpp register the function with
NBLA_REGISTER_FUNCTION_SOURCE(..)
macro.
Delete the auto generated setup_impl,forward_impl,backward_impl functions from generated ${snake_name}.cpp. - Update the ${snake_name}.hpp with new function implementation using element-wise macro.
NBLA_DEFINE_TRANSFORM_UNARY
macro is used for element-wise operations.
Please refer implementation of add_scalar function at cpp file and hpp file.
The template files generated using cmake contains the class definition for new function. The new function class inherits from the abstract base class BaseFunction
which in turn inherits from Function
class.
In order to add new layer function to nnabla you must implement setup, forward, and backward functions.
Implement the following three functions in the C++ file by adding your function logic:
- void setup_impl(const Variables &inputs, const Variables &outputs):
Usually this function is used to initialize output parameters.
The setup_impl is called only once during the forward pass of the training.
It can be called more than once during the backward pass of the training.
It is recommended to Keep the setup_impl implementation light weight. - void forward_impl(const Variables &inputs, const Variables &outputs):
In this function implement the new function logic for the forward pass. - void backward_impl(const Variables &inputs, const Variables &outputs, const vector &propagate_down, const vector &accum):
In this function implement the new function logic for the backward pass.
Apart from input and output parameters, the backward_impl() function comes with 2 special parameters. I.e.propagate_down
: The parameterpropagate_down
tells whether the gradients for the backward pass needs to be calculated or not. If its value is false then no need to calculate the gradients, the function can simply return.accum
: Theaccum
parameter tells whether to accumulate gradients from previous layer or not. If it is true then accumulate by summing the gradients. If it is set to false then just update the gradients.
Note: The variable names must be valid names for Cython, Python and C++.
For example, the variable name lambda cannot be used for keywords, since it is an invalid name in Cython and Python, although it is valid in C++.
Optionally, if you need the higher-order gradients of the new function added,
- Implement def ${snake_name}_backward(self, inputs, **kwargs) in
python/src/nnabla/backward_function/${snake_name}.py
:
This method implements the backward pass of the function added in the python-layer, meaning the gradients of the function. See sigmoid.py, relu.py, and/or affine.py for details. - Call nnabla.backward_functions.register(<FunctionName>, ${snake_name}_backward):
If you add the new function based on the functions.yaml, this registration is automatically done at the compile-time, you do not need to do any registration. If you add a new PythonFunction (python-layer-only way to add a function), you have to do registration manually. See backward_functions.py.tmpl and backward_function.py for more details.
It is good practice to test your new function implementation by writing the unit test cases.
You can add unit test cases for C++ version of your function implementation as well as for python version.
For adding C++ unit test cases:
All C++ based unit-tests are found here.
NNabla uses Google Test
unit test framework for C++ unit testing. You can learn more about Google Test
from here.
Following are steps for adding C++ based test cases:
- Design your test cases.
- Create test_${snake_name}.cpp under nnabla/src/nbla/test folder.
- Implement the designed test cases in test_${snake_name}.cpp.
- //TODO need to write how to execute C++ unit test without using nnabla-builder.
For adding python unit test cases:
All python based function unit-tests are found here.
NNabla uses pytest
unit test framework for python unit testing. You can learn more about pytest from here.
Following are steps for adding C++ based test cases:
- Design your test cases.
- Create test_${snake_name}.py under
nnabla/python/test/function
folder. - Implement the designed test cases in test_${snake_name}.py
- Execute the test cases by following instructions under
Verify installation by unit testing
section of Build Manuals.
NOTE:
Unit testing in C++ is often demanded by embedded engineers.
But writing unit testing in C++ requires more effort, and imposes double-maintenance cost over Python unit-tests.
Also, testing in Python is much easier to debug.
To reduce the effort required for C++ test-case implementation, currently we are thinking of creating a framework which can generate C++ unit test code of each functions from our Python testing framework (function_tester
).
However, we do not have any specific plan to add this feature soon. Contributions are welcome.
Write benchmark test cases to measure the performance of your new function implementation.
It is optional to write benchmark test cases, but it is good practice to measure the performance of your new function.
Following are steps for adding benchmark python based test cases:
- Create the test_${snake_name}.py file at
python/benchmark/function
folder (i.e. here). - Implement the benchmark related test cases into test_${snake_name}.py file.
- Please follow the instructions in Benchmark readme for the execution of the benchmark test cases.
It is very important to explain the behavior of input and output parameters of your new function implementation.
You can add the new function documentation to following files to generate the usage document.
- function_types.yaml.
include/nbla/function/${snake_name}.hpp
doc/python/api/function.rst
You need to edit the doc/python/api/function.rst
(i.e. here) to make docs appear in the Python API docs.
Add the new layer function entry manually in function.rst file for including documentation in the Python API page.
After editing the above files with details of new function usage, compile the files to generate the documents.
cd ${nnabla_root_directory}
mkdir ${doc_dir_name}
cd ${doc_dir_name}
cmake -DPYTHON_VERSION=3.10 -DBUILD_CPP_LIB=ON -DBUILD_CPP_UTILS=OFF -DNNABLA_UTILS_STATIC_LINK_DEPS=ON -DBUILD_PYTHON_PACKAGE=ON ..
cd ..
make -C ${doc_dir_name} all wheel doc
The above command will generate doxygen based documentation and sphinx documentation under ${nnabla_root_directory}/${doc_dir_name}/doc/
.
We know many developers use CUDA to accelerate processing speed. See contribution guide of CUDA extension.