0. Overview

Before adding Op, please refer to Op Manual to avoid unnecessary duplication.

In MNN, adding Op consists of the following steps:

  1. Add model description
  2. Add model conversion
  3. Add shape calculation
  4. Add implementation

1. Add model description

After modifying the model description, you need to run the generate script to regenerate the model description header file.

Add Op Type

Append the operator name to the OpType list in schema/default/MNN.fbs, such as:

  1. enum OpType : int {
  2. AbsVal,
  3. QuantizedAdd,
  4. ...
  5. MyCustomOp
  6. }

Add Op Parameter

If the operator does not contain parameters, you can skip this step.

First, append the operator parameter name to the OpParameter list in schema/default/MNN.fbs, such as:

  1. union OpParameter {
  2. QuantizedAdd,
  3. ArgMax,
  4. AsString,
  5. ...
  6. MyCustomOpParam
  7. }

Then add a parameter description. If the operator is from Caffe, choose CaffeOps.fbs; if the operator is from TensorFlow, use TensorflowOp.fbs.

  1. table MyCustomOpParam {
  2. padX:int;
  3. padY:int;
  4. kernelX:int;
  5. kernelY:int;
  6. strideX:int;
  7. strideY:int;
  8. dataType:DataType=DT_FLOAT;
  9. }

2. Add model conversion

After adding the model conversion, you need to re-run cmake.

Currently, MNN supports the conversion from TensorFlow, TensorFlow Lite, Caffe and ONNX model.

Tensorflow Model Convert

  1. Add conversion class
    Add MyCustomOpTf.cpp under tools/converter/source/tensorflow. You can declare the conversion class directly, or you can use a macro definition to simplify the code.

Direct declaration example:

  1. class MyCustomOpTf : public tfOpConverter {
  2. public:
  3. virtual void run(MNN::OpT *dstOp, TmpNode *srcNode, TmpGraph *tempGraph);
  4. MyCustomOpTf() {}
  5. virtual ~MyCustomOpTf() {}
  6. virtual MNN::OpType opType();
  7. virtual MNN::OpParameter type();
  8. }

Equivalent macro definition example:

  1. DECLARE_OP_CONVERTER(MyCustomOpTf);

Need to implement run, destructor, opType and type functions. Among them, the run function is used to parse the model’s proto file to get the parameters, and then assign them to the flatbuffer custom parameters. The parameter srcNode holds the input and output node information, and the TmpNode can be found in the tempGraph according to the input and output nodes. Call the function find_attr_value(const tensorflow::NodeDef&, const char*, tensorflow::AttrValue&) to get the value of the corresponding parameter.

Register conversion class:

  1. REGISTER_CONVERTER(MyCustomOpTf, MyCustomOp);
  1. Add mapping
    Add the corresponding TensorFlow Op name to the MNN Op name mapping in OpMapper.hpp:

    1. {"OpName1", MNN::OpType_MyCustomOp},
    2. {"OpName2", MNN::OpType_MyCustomOp},
  2. Handling Op with Const
    If Const is not treated as a parameter of Op, but as a separate Op, you can ignore this step; if Const is treaded as a parameter of Op, modify the function _genMinGraph() in TmpGraph.cpp , set isCovered property of corresponding Const node to true.

Tensorflow Lite Model Convert

  1. Add conversion class
    Add MyCustomOpTflite.cpp under tools/converter/source/tflite.

Macro definition example:

  1. DECLARE_OP_COVERTER(MyCustomOpTflite);

Need to implement functions:

  1. MyCustomOpTflite::opType(bool quantizedModel);
  2. MyCustomOpTflite::type(bool quantizedModel);
  3. MyCustomOpTflite::run(MNN::OpT *dstOp,
  4. const std::unique_ptr<tflite::OperatorT> &tfliteOp,
  5. const std::vector<std::unique_ptr<tflite::TensorT> > &tfliteTensors,
  6. const std::vector<std::unique_ptr<tflite::BufferT> > &tfliteModelBuffer,
  7. const std::vector<std::unique_ptr<tflite::OperatorCodeT> > &tfliteOpSet,
  8. bool quantizedModel)

Among them, the run function has one more quantizedModel parameter than the version of TensorFlow. If the quantizedModel is true, the model is a quantitative model and needs to be converted to the corresponding quantified Op; if it is false, it is converted to floating point Op. In the run function, you need to set the index of the input and output tensor:

  1. // set input output index
  2. dstOp->inputIndexes.resize(1);
  3. dstOp->outputIndexes.resize(1);
  4. dstOp->inputIndexes[0] = tfliteOp->inputs[0];
  5. dstOp->outputIndexes[0] = tfliteOp->outputs[0];

Register conversion class:

  1. using namespace tflite;
  2. REGISTER_CONVERTER(MyCustomOpTflite, BuiltinOperator_OPName);

Caffe Model Convert

  1. Add conversion class
    Add MyCustomOp.cpp under/tools/converter/source/caffe.

Class declaration example:

  1. class MyCustomOp : public OpConverter {
  2. public:
  3. virtual void run(MNN::OpT* dstOp,
  4. const caffe::LayerParameter& parameters,
  5. const caffe::LayerParameter& weight);
  6. MyCustomOp() {}
  7. virtual ~MyCustomOp() {}
  8. virtual MNN::OpType opType();
  9. virtual MNN::OpParameter type();
  10. };

Implement the run, opType, and type functions, and parse the caffe parameter in the run function to get the specific parameters. The parameters parameter stores the parameter information of Op, and the weight stores data parameters such as convolution and BN.

Register conversion class:

  1. static OpConverterRegister<MyCustomOp> a("MyCustomOp");

ONNX Model Convert

  1. Add conversion class
    Add MyCustomOpOnnx.cpp under/tools/converter/source/onnx.

Macro definition example:

  1. DECLARE_OP_CONVERTER(MyCustomOpOnnx);

Need to implement functions:

  1. MNN::OpType MyCustomOpOnnx::opType();
  2. MNN::OpParameter MyCustomOpOnnx::type();
  3. void MyCustomOpOnnx::run(MNN::OpT* dstOp,
  4. const onnx::NodeProto* onnxNode,
  5. std::vector<const onnx::TensorProto*> initializers);

In the run function, onnxNode contains the ONNX original node information. weight and other data information needs to be taken from the initializers.

Register conversion class:

  1. REGISTER_CONVERTER(MyCustomOpOnnx, MyCustomOp);

3. Add shape calculation

After adding the shape calculation code, you need to re-run cmake.

  1. Add calculation class
    Add ShapeMyCustomOp.cpp to /source/shape.
    1. class MyCustomOpSizeComputer : public SizeComputer {
    2. public:
    3. virtual bool onComputeSize(const MNN::Op* op, const std::vector<Tensor*>& inputs,
    4. const std::vector<Tensor*>& outputs) const override {
    5. // set tensor->buffer.type
    6. // .dimensions
    7. // .dim[x].extent
    8. // .dim[x].stride
    9. // .dim[x].flag
    10. return true;
    11. }
    12. virtual float onComputeFlops(const MNN::Op* op,
    13. const std::vector<Tensor*>& inputs,
    14. const std::vector<Tensor*>& outputs) const {
    15. return flops_for_calc_output_from_input;
    16. }
    17. };
    In the onComputeSize function, the dimension information of the output tensor is calculated according to the dimension information of the input tensor, and the data type of the output tensor is set. Returns true if the calculation is succeed; returns false if the input dimension information is unknown.
    In the onComputeFlops function, the total calculation amount is returned according to the dimension information of the input and output tensor.

Register calculation class:

  1. REGISTER_SHAPE(MyCustomOpSizeComputer, OpType_MyCustomOp);

4. Add Implementation

Add CPU Implementation

Add CPUMyCustomOp.hpp and CPUMyCustomOp.cpp to source/backend/CPU.

  1. Implementation class declaration

    1. class CPUMyCustomOp : public Execution {
    2. public:
    3. virtual ErrorCode onResize(const std::vector<Tensor *> &inputs,
    4. const std::vector<Tensor *> &outputs) override;
    5. virtual ErrorCode onExecute(const std::vector<Tensor *> &inputs,
    6. const std::vector<Tensor *> &outputs) override;
    7. };
  2. Implement onResize and onExecute

In onResize, call backend()->onAcquireBuffer(&mCache, Backend::DYNAMIC) to allocate the cache, and call backend()->onReleaseBuffer(&mCache, Backend::DYNAMIC) to reclaim the cache. The released memory can be reused.
In onExecute, doing the necessary input checks will help you find the problem ahead of time. If the execution is completed, it returns NO_ERROR correctly.

  1. Register implementation class
    1. class CPUMyCustomOpCreator : public CPUBackend::Creator {
    2. public:
    3. virtual Execution *onCreate(const std::vector<Tensor *> &inputs,
    4. const std::vector<Tensor *> &outputs,
    5. const MNN::Op *op,
    6. Backend *backend) const override {
    7. return new CPUMyCustomOp(backend);
    8. }
    9. };
    10. REGISTER_CPU_OP_CREATOR(CPUMyCustomOpCreator, OpType_MyCustomOp);

Add Metal Implementation

  1. Add Shader
    Add MetalMyCustomOp.metal in the source/backend/Metal directory and add it to the Xcode project. Metal shader can refer to the existing implementations.

  2. Implementation class declaration
    Add MetalMyCustomOp.hpp and MetalMyCustomOp.cpp in the source/backend/Metal directory and add them to the Xcode project:

    1. class MetalMyCustomOp : public Execution {
    2. public:
    3. virtual ErrorCode onResize(const std::vector<Tensor *> &inputs,
    4. const std::vector<Tensor *> &outputs) override;
    5. virtual ErrorCode onExecute(const std::vector<Tensor *> &inputs,
    6. const std::vector<Tensor *> &outputs) override;
    7. };
  3. Implement onResize and onExecute
    Unlike CPU Tensor, which stores data in the host pointer, the Metal data pointer is stored in the deviceId, and the deviceId stores the id<MTLBuffer>:

    1. auto buffer = (__bridge id<MTLBuffer>)(void *)tensor->deviceId();

Specific parameters of Metal Op can be stored by id<MTLBuffer>. Different from tensor, the buffer can mix multiple data types, just ensure that the correct length is specified when creating. E.g:

  1. auto buffer = [context newDeviceBuffer:2 * sizeof(int) + 2 * sizeof(__fp16) access:CPUWriteOnly];
  2. ((__fp16 *)buffer.contents)[0] = mAlpha / mLocalSize; // alpha
  3. ((__fp16 *)buffer.contents)[1] = mBeta; // beta
  4. ((int *)buffer.contents)[1] = mLocalSize; // local size
  5. ((int *)buffer.contents)[2] = inputs[0]->channel(); // channel

When creating a buffer, you need to specify access control permissions. There are currently three permissions:

  • CPUReadWrite, data is shared between CPU/GPU, generally used for device buffer;
  • CPUWriteOnly, the data is not read after being written by the CPU, and is generally used for the parameter buffer;
  • CPUTransparent, the data is only in the GPU, generally used in the heap buffer.

MNNMetalContext has two sets of similar interfaces to create buffer, the difference is only in the life cycle of the data:

  • the memory occupied by the device will not be reused in the single inference process;
  • the memory occupied by the heap is reused by other Ops after calling -[MNNMetalContext releaseHeapBuffer:].

In general, the heap will only be used with CPUTransparent. heap only aviliable on iOS 10+,fall back to device on iOS 9

When using Metal, It is forbidden to create device and library yourself if it is not a special case. Loading the library and compiling the function are time-consuming behaviors, and MNNMetalContext does the necessary cache optimization. An example of executing Metal via context is as follows:

  1. auto context = (__bridge MNNMetalContext *)backend->context();
  2. auto kernel = /* metal kernel name NSString */;
  3. auto encoder = [context encoder];
  4. auto bandwidth = [context load:kernel encoder:encoder];
  5. /* encoder set buffer(s)/sampler(s) */
  6. [context dispatchEncoder:encoder
  7. threads:{x, y, z}
  8. maxThreadsPerGroup:maxThreadsPerThreadgroup]; // recommended way to dispatch
  9. [encoder endEncoding];
  1. Register implementation class
    1. class MetalMyCustomOpCreator : public MetalBackend::Creator {
    2. public:
    3. virtual Execution *onCreate(const std::vector<Tensor *> &inputs,
    4. const MNN::Op *op, Backend *backend) const {
    5. return new MetalMyCustomOp(backend);
    6. }
    7. };
    8. REGISTER_METAL_OP_CREATOR(MetalMyCustomOpCreator, OpType_MyCustomOp);

Add Vulkan Implementation

  1. Add Shader
    Add a shader (*.comp) in the source/backend/vulkan/execution/glsl directory. Uses image as data container if the input memory layout is NC4HW4, uses buffer otherwise. You can refer to the existing implementations. Then, execute the makeshader.py script to compile shaders.

  2. Implementation class declaration
    Add VulkanMyCustomOp.hpp and VulkanMyCustomOp.cpp to source/backend/vulkan/execution/:

    1. class VulkanMyCustomOp : public VulkanBasicExecution {
    2. public:
    3. VulkanMyCustomOp(const Op* op, Backend* bn);
    4. virtual ~VulkanMyCustomOp();
    5. ErrorCode onEncode(const std::vector<Tensor*>& inputs,
    6. const std::vector<Tensor*>& outputs,
    7. const VulkanCommandPool::Buffer* cmdBuffer) override;
    8. private:
    9. // GPU Shader Parameters
    10. std::shared_ptr<VulkanBuffer> mConstBuffer;
    11. // Pipeline
    12. const VulkanPipeline* mPipeline;
    13. // Layout Descriptor Set
    14. std::shared_ptr<VulkanPipeline::DescriptorSet> mDescriptorSet;
    15. };
  3. Implement onEncode
    To implement the function onEncode, you first need to do a memory layout check: if it is NC4HW4, uses image as data container, otherwise uses buffer. Return NO_ERROR after execution.

  4. Register implementation class

    1. class VulkanMyCustomOpCreator : public VulkanBackend::Creator {
    2. public:
    3. virtual Execution* onCreate(const std::vector<Tensor*>& inputs,
    4. const MNN::Op* op,
    5. Backend* backend) const override {
    6. return new VulkanMyCustomOp(op, backend);
    7. }
    8. };
    9. static bool gResistor = []() {
    10. VulkanBackend::addCreator(OpType_MyCustomOp, new VulkanMyCustomOpCreator);
    11. return true;
    12. }();

Add OpenCL Implementation

  1. Add Kernel
    Add a specific kernel (*.cl) to source/backend/opencl/execution/cl. Currently feature maps are implemented using image2d. You can refer to the existing implementations. Then execute opencl_codegen.py to generate the kernel map.

2.Implementation class declaration
Add MyCustomOp.h and MyCustomOp.cpp to source/backend/opencl/execution/:

  1. template <typename T>
  2. class MyCustomOp : public Execution {
  3. public:
  4. virtual ErrorCode onResize(const std::vector<Tensor *> &inputs,
  5. const std::vector<Tensor *> &outputs) override;
  6. virtual ErrorCode onExecute(const std::vector<Tensor *> &inputs,
  7. const std::vector<Tensor *> &outputs) override;
  8. };
  1. Implement onResize and onExecute
    Implement the function onResize (optional), onExecute. Return NO_ERROR after execution.

  2. Register implementation class

    1. OpenCLCreatorRegister<TypedCreator<MyCustomOp<cl_data_t>>> __my_custom_op(OpType_MyCustomOp);

Add OpenGL Implementation

  1. Add Shader
    Add a shader (*.glsl) under source/backend/opengl/glsl, no header file is needed. The feature map is represented by image3d. You can refer to the existing implementations. Then, execute makeshader.py under source/backend/opengl.

  2. Add Executor
    Add GLMyCustomOp.h and GLMyCustomOp.cpp to source/backend/opengl/execution/: ```cpp class GLMyCustomOp : public Execution { public: GLMyCustomOp(const std::vector &inputs, const Op op, Backend bn); virtual ~GLMyCustomOp(); virtual ErrorCode onExecute(const std::vector &inputs,

    1. const std::vector<Tensor *> &outputs) override;

    virtual ErrorCode onResize(const std::vector &inputs,

    1. const std::vector<Tensor *> &outputs) override;

private: std::shared_ptr mProgram; };

  1. 3. Implement `onResize` and `onExecute`<br />Implement the function `onResize` (optional), `onExecute`. Return NO_ERROR after execution.
  2. 4. Register implementation class
  3. ```cpp
  4. GLCreatorRegister<TypedCreator<GLMyCustomOp>> __my_custom_op(OpType_MyCustomOp);