|
Snapdragon Neural Processing Engine SDK
Reference Guide
|
This tutorial goes through the complete user-defined layer workflow from training a model with a newly-defined layer to implementing the custom layer with SNPE's UDL APIs.
Note:
SNPE currently only supports user-defined layers in Caffe.
UDL is deprecated from snpe-1.35.0. Check out Overview of UDO for a newer version of this feature.
The following figure shows an overview of the modifications needed to support user-defined layers that are unknown to the SNPE runtime and converters.
There are two main pieces that need to be implemented to add support for user-defined layers:

For this tutorial, the existing Scale layer implementation in Caffe will be used as the basis. ScaleParameter prototxt will be re-used to carry the parameters for the custom layer.
Create copies of the Scale layer code
cp $CAFFE_HOME/include/caffe/layers/scale_layer.hpp $CAFFE_HOME/include/caffe/layers/mycustomscale_layer.hpp cp $CAFFE_HOME/src/caffe/layers/scale_layer.cpp $CAFFE_HOME/src/caffe/layers/mycustomscale_layer.cpp cp $CAFFE_HOME/src/caffe/layers/scale_layer.cu $CAFFE_HOME/src/caffe/layers/mycustomscale_layer.cu
Edit $CAFFE_HOME/include/caffe/layers/mycustomscale_layer.hpp with the following modifications:
Edit $CAFFE_HOME/src/caffe/layers/mycustomscale_layer.cpp with the following modifications:
Edit $CAFFE_HOME/src/caffe/layers/mycustomscale_layer.cu with the following modifications:
cd $CAFFE_HOME make all make distribute make pycaffe
Next, add MyCustomScale to the MNIST LeNet model to incorporate the MyCustomScale layer and train it to generate a caffemodel file.
Follow the LeNet on MNIST Caffe tutorial at http://caffe.berkeleyvision.org/gathered/examples/mnist.html to train the unmodified model. This is also required to fetch the MNIST training and testing datasets.
Create copies of the LeNet model and training scripts
cd $CAFFE_HOME/examples/mnist cp lenet.prototxt mycustomlenet.prototxt cp lenet_train_test.prototxt mycustomlenet_train_test.prototxt cp lenet_solver.prototxt mycustomlenet_solver.prototxt cp train_lenet.sh train_mycustomlenet.sh
Edit mycustomlenet.prototxt with the following modifications:
layer {
name: "data"
type: "Input"
top: "data"
input_param { shape: { dim: 1 dim: 1 dim: 28 dim: 28 } }
}
layer {
bottom: "ip1"
top: "scale"
name: "scale"
type: "MyCustomScale"
scale_param {
bias_term: false
}
}
layer {
name: "ip2"
type: "InnerProduct"
bottom: "scale"
top: "ip2"
...
Edit mycustomlenet_train_test.prototxt with the following modifications:
layer {
bottom: "ip1"
top: "scale"
name: "scale"
type: "MyCustomScale"
scale_param {
bias_term: false
filler: { value: 0 }
}
}
Note that scale_param is re-used to carry MyCustomScale's parameters. layer {
name: "ip2"
type: "InnerProduct"
bottom: "scale"
top: "ip2"
...
Edit mycustomlenet_solver.prototxt with the following modifications:
Edit train_mycustomlenet.sh to take in the mycustomlenet_solver.prototxt solver file.
cd $CAFFE_HOME ./examples/mnist/train_mycustomlenet.shThe training will generate the weights into mycustomlenet_iter_10000.caffemodel file. This in combination with mycustomlenet.prototxt model definition file will be used to generate the SNPE dlc model.
Caffe models are converted to DLC models using the snpe-caffe-to-dlc executable. Support for user-defined layers is added by passing the [–udl] parameter with the filename and factory function of your udl python module.
To convert the Caffe model generated earlier with the MyCustomScale layer, run the following command:
Note: Follow setup instructions for setting SNPE_ROOT with CAFFE before running the following.
export PYTHONPATH=$PYTHONPATH:$SNPE_ROOT/examples/Python/UdlExample
python snpe-caffe-to-dlc \
--input_network $CAFFE_HOME/examples/mnist/mycustomlenet.prototxt \
--caffe_bin $CAFFE_HOME/examples/mnist/mycustomlenet_iter_10000.caffemodel \
---udl my_udl_layers udl_factory_func
--output_path mycustomlenet.dlc
View the model details with snpe-dlc-info.
snpe-dlc-info -i mycustomlenet.dlc
Note the details of the user-defined layer.
... | 5 | ip1 | fc | pool2 | ip1 | 1x500 | param count: 400k (92.9%) | | | | | | | | MACs: 400k (17.4%) | | 6 | relu1 | neuron | ip1 | relu1.ip1 | 1x500 | a: 0 | | | | | | | | b: 0 | | | | | | | | min_clamp: 0 | | | | | | | | max_clamp: 0 | | | | | | | | func: relu | | 7 | scale | user_defined | relu1.ip1 | scale | 1x500 | blob_size: 2017 | | 8 | ip2 | fc | scale | ip2 | 1x10 | param count: 5k (1.16%) | | | | | | | | MACs: 5k (0.218%) | | 9 | prob | softmax | ip2 | prob | 1x10 | |
Examine $SNPE_ROOT/examples/Python/UdlExample/my_udl_layers.py:
udl_factory_func: the –udl parameters expects users to pass Filename, Function name.
For our example, the Filename is my_udl_layers and Function name is udl_factory_func.
The UDL handler map (udl_supported_types) is a dictionary where the key is the layer name and the value is the handler class for that layer.
The UDL handler instance (udl_mycustomscale) needs a callback function and the expected input/output axes order of your custom layer implementation.
The handler callback function for a custom layer receives the following parameters:
The handler function returns:
For MyCustomScale layer, the output dimensions are the same as the input dimensions.
The UdlBlobMyCustomScale is a helper class used by udl_mycustomscale_func to generate a packed buffer that contains all the layer params and weights. UdlBlobMyCustomScale delegates the packing to another sub-class.
The remaining of UdlBlobMyCustomScale class's init method fills out the fields that need to be packed. For MyCustomScale, the only layer parameter is bias_term. Note that the handler function did not receive any layer weights, they come from the converter through weight_provider. The blob2arr() utility method provided by SNPE converts the Caffe blob to a linear array of floats.
Finally, call serialize() to generate the packed data.
This section outlines the code changes necessary to implement a user-defined layer called by SNPE runtime.
A native example that implements the MyCustomScale layer is located at $SNPE_ROOT/examples/NativeCpp/UdlExample. This example extends the native BatchRun utility to support MyCustomScale layer via SNPE's UDL APIs.
jni/main.cpp:
The setUdlBundle() method in SNPEBuilder is passed a UDLBundle object. This bundle object contains two fields:
The UDL factory method receives the opaque data passed in earlier (cookie) and a UDLContext object. The UDLContext object contains all the available information about the custom layer.
The UDL factory method parses the layer data to retrieve the layer type. Once the layer type is known the method instantiates and returns a pointer to the layer object.
The function for parsing the layer type from the packed layer data is defined in MyUdlLayers.hpp. It reads the first integer from the buffer, which denotes the layer type.
Finally the factory method instantiates the requested layer.
udlMyCustomScaleLayer.hpp
A UDL implementation must extend the interface zdl::DlSystem::IUDL. This interface defined the following methods that need to be implemented by the user:
Further, in this example a private method ParseMyCustomLayerParams() is used to handle the parsing of the packed data from the DLC model.
udlMyCustomScaleLayer.cpp
The UdlMyCustomScale::ParseMyCustomLayerParams() method handles the parsing of the packed layer data to initialize the parameters and weights for MyCustomScale layer. The unpacking is performed in the same order as the packing in MyUdlLayers.py.
The UdlMyCustomScale::Setup() method performs layer initialization. It receives the following parameters:
It first checks the number of inputs and outputs. In this example a single-input single-output scale layer is implemented.
Next it verifies that the total size of input matches the total size of the output as the Scale operation is one-to-one.
Then it calls the ParseMyCustomLayerParams() method to read the packed layer data and initialize the custom layer.
Finally, the method verifies that bias_term is set to false since this example only implements the multiplicative scale operation.
The UdlMyCustomScale::Execute() method implements the forward pass for the custom layer.
It scales each input value by the corresponding weight.
With all the runtime pieces in place, BatchRun can be built with support for MyCustomScale user-defined layer.
First move to UDL example base directory.
cd $SNPE_ROOT/examples/NativeCpp/UdlExample
Run the following command to compile for x86 Linux targets.
make -f Makefile.x86_64-linux-clang
To build with clang/libc++ SNPE binaries (i.e., arm-android-clang6.0 and aarch64-android-clang6.0), use the following command:
ndk-build NDK_TOOLCHAIN_VERSION=clang APP_STL=c++_shared
The UDL-enabled BatchRun can be run on a Linux Host with the UDL model generated earlier. Sample inputs of hand-written characters are provided in $SNPE_ROOT/models/mnist/data.
cd $SNPE_ROOT/models/mnist/data $SNPE_ROOT/examples/NativeCpp/UdlExample/obj/local/x86_64-linux-clang/snpe-net-run-udl \ --container $SNPE_ROOT/examples/Python/UdlExample/mycustomlenet.dlc \ --input_list image_list.txt
The MyCustomScale layer's Setup() method should output information about the layer parameters and weights:
UdlMyCustomScale::Setup() of name scale UdlMyCustomScale::Setup() input size dim: 500, output: 500 UdlMyCustomScale::Setup() got blob size 2017 UdlMyCustomScale::Setup() bias_term=0 UdlMyCustomScale::Setup() weight dimensions: (500,) UdlMyCustomScale::Setup() # weights=500 ...
The outputs can be verified with the following script:
python3 ../scripts/interpretRawLeNetOutput.py output/Result_0/prob.raw python3 ../scripts/interpretRawLeNetOutput.py output/Result_1/prob.raw python3 ../scripts/interpretRawLeNetOutput.py output/Result_2/prob.raw python3 ../scripts/interpretRawLeNetOutput.py output/Result_3/prob.raw
The classification results should be 0, 3, 5 and 9, respectively.