5.8. Custom operations
PopRT supports creation of custom operators. You will need to create a custom operation when your model contains an operator that is not supported in PopRT. In this case, you can write a custom operator and compile it into a dynamic link library. PopRT supports dynamic linking of this custom operator into PopRT with the command line.
Since PopRT uses PopART as the backend, the process of developing custom operators for PopRT is the same as that for PopART. Please refer to Creating Custom Op in PopART .
This section describes the process of developing custom operators for PopRT with an example.
5.8.1. Writing custom operators
Taking the custom operator named LeakyRelu
as an example, you first need to write the C++ code for it:
1// Copyright (c) 2020 Graphcore Ltd. All rights reserved.
2
3// This example demonstrates how to create a custom operator for PopART, in this
4// case a Leaky ReLU op that returns `x` for any element `x >= 0` and `x *
5// alpha` for any element `x < 0`, where `alpha` is provided as a scalar
6// attribute to the operator.
7#include <popart/operatoridentifier.hpp>
8#include <popart/opmanager.hpp>
9#include <popart/opserialiser.hpp>
10#include <popart/popx/opxmanager.hpp>
11
12#include <popops/ElementWise.hpp>
13#include <popart/popx/opx.hpp>
14
15namespace CustomOperators {
16const popart::OperatorIdentifier LeakyReluId = {popart::Domain::ai_graphcore,
17 "LeakyRelu",
18 1};
19} // namespace CustomOperators
20
21class LeakyReluOp;
22class LeakyReluOpx;
23
24class LeakyReluOp : public popart::Op {
25public:
26 LeakyReluOp(const popart::OperatorIdentifier &_opid,
27 float _alpha,
28 const popart::Op::Settings &settings_)
29 : popart::Op(_opid, settings_), alpha(_alpha) {}
30
31 std::unique_ptr<Op> clone() const final {
32 return std::make_unique<LeakyReluOp>(*this);
33 }
34
35 void setup() final { outInfo(0) = inInfo(0); }
36
37 void appendAttributes(popart::OpSerialiserBase &os) const override {
38 Op::appendAttributes(os);
39 os.appendAttribute("alpha", getAlpha());
40 }
41
42 void appendOutlineAttributes(popart::OpSerialiserBase &os) const override {
43 Op::appendOutlineAttributes(os);
44 os.appendAttribute("alpha", getAlpha());
45 }
46
47 float getSubgraphValue() const final { return getHighSubgraphValue(); }
48
49 bool requiresRandomSeed() const override { return false; }
50
51 // Attributes
52 float getAlpha() const { return alpha; }
53
54private:
55 float alpha;
56};
57
58namespace {
59using popart::DataType;
60using popart::OpDefinition;
61
62static OpDefinition::DataTypes T = {DataType::FLOAT16, DataType::FLOAT};
63
64static OpDefinition
65 leakyReluOpDef({OpDefinition::Inputs({{"input", T}}),
66 OpDefinition::Outputs({{"output", T}}),
67 OpDefinition::Attributes({{"alpha", {"*"}}})});
68
69static popart::OpCreator<LeakyReluOp> leakyReluOpCreator(
70 popart::OpDefinitions({{CustomOperators::LeakyReluId, leakyReluOpDef}}),
71 [](const popart::OpCreatorInfo &info) {
72 // default alpha is 10**(-2)
73 float alpha = info.attributes.getAttribute<popart::Attributes::Float>(
74 "alpha", 1e-2f);
75 return std::make_unique<LeakyReluOp>(info.opid, alpha, info.settings);
76 },
77 true);
78} // namespace
79
80namespace pe = popops::expr;
81
82class LeakyReluOpx : public popart::popx::Opx {
83public:
84 LeakyReluOpx(popart::Op *op, popart::popx::Devicex *devicex)
85 : popart::popx::Opx(op, devicex) {
86 verifyOp<LeakyReluOp>(op, {CustomOperators::LeakyReluId});
87 }
88
89 void grow(poplar::program::Sequence &prog) const final {
90
91 auto op = getOp<LeakyReluOp>();
92
93 poplar::Tensor input = getInTensor(0);
94
95 float alpha = op.getAlpha();
96
97 // x < 0.0f ? alpha * x : x
98 auto expression = pe::Select(pe::Mul(pe::Const(alpha), pe::_1),
99 pe::_1,
100 pe::Lt(pe::_1, pe::Const(0.0f)));
101
102 popops::mapInPlace(graph(),
103 expression,
104 {input},
105 prog,
106 debugContext("LeakyRelu"),
107 poplar::OptionFlags());
108
109 setOutTensor(0, input);
110 }
111};
112
113static popart::popx::OpxCreator<LeakyReluOpx>
114 LeakyReluOpxCreator({CustomOperators::LeakyReluId});
Download leaky_relu_custom_op.cpp
Create a Makefile and generate custom_ops.so
using the make
command:
1CXX ?= g++
2CXXFLAGS = -std=c++14 -fPIC -g
3LDLIBS = -shared -lpopart
4ONNX_NAMESPACE = -DONNX_NAMESPACE=onnx
5
6BUILD_DIR = build
7SOURCES = leaky_relu_custom_op.cpp
8TARGET = $(BUILD_DIR)/custom_ops.so
9
10all: create_build_dir leaky_relu_custom_op
11
12.PHONY: create_build_dir
13create_build_dir:
14 mkdir -p $(BUILD_DIR)
15
16leaky_relu_custom_op: leaky_relu_custom_op.cpp
17 $(CXX) $(SOURCES) $(LDLIBS) $(CXXFLAGS) $(ONNX_NAMESPACE) -o $(TARGET)
18
19.PHONY: clean
20clean:
21 rm -rf $(BUILD_DIR)
Create a shape-inference file for the custom operator:
1# Copyright (c) 2022 Graphcore Ltd. All rights reserved.
2from typing import Tuple
3
4import onnx
5import onnx.helper
6import onnx.shape_inference
7
8from poprt.passes import ShapeFunc, get_dtype, get_shape, register_shape_func
9
10
11@register_shape_func(['LeakyRelu'])
12class LeakyRelu(ShapeFunc):
13 """Function based on ONNX to infer the shape and dtype of custom op."""
14
15 def __init__(self) -> None:
16 super().__init__()
17
18 def __call__(
19 self,
20 model: onnx.ModelProto,
21 node: onnx.NodeProto,
22 ) -> Tuple[onnx.ModelProto, bool]:
23 graph = model.graph
24 input_name = node.input[0]
25 output_name = node.output[0]
26 # If the Op already has known shape and dtype of output, return True
27 if get_shape(model.graph, output_name) and get_dtype(model.graph, output_name):
28 return model, True
29
30 input_dtype = get_dtype(graph, input_name)
31 input_shape = get_shape(graph, input_name)
32 # If the Op is able to be inferred shape and dtype, return True
33 if input_dtype and input_shape and 0 not in input_shape:
34 # ![Shape-Inference Function begin]
35
36 # Step.1: Write the method following ONNX-Protobuf standard,
37 # to calc shape and dtype of output in terms of shape and dtype of input
38 # The LeakyRelu Op has same shape and dtype with input and output
39
40 # Step.2: Create new TensorProto with inferred shape and dtype of output
41 output_tensor = onnx.helper.make_tensor_value_info(
42 output_name, input_dtype, input_shape
43 )
44 # Step.3: Call update_value_info to update
45 model = self.update_value_info(model, output_tensor)
46 # Step.4: Call infer_shapes function
47 model = onnx.shape_inference.infer_shapes(model)
48 # ![Shape-Inference Function end]
49 return model, True
50 # If the Op is not able to be inferred, return False
51 else:
52 return model, False
Download custom_shape_inference.py
Create the ONNX model file with the LeakyRelu
op
Run the following test code with Python3 to generate the ONNX model file custom_op_test.onnx
for testing:
1# Copyright (c) 2022 Graphcore Ltd. All rights reserved.
2import argparse
3import os
4
5import onnx
6
7from onnx import helper
8
9
10def create_onnx_model_with_custom_op():
11 TensorProto = onnx.TensorProto
12
13 attributes = {"alpha": 0.01}
14 leaky_relu = helper.make_node(
15 "LeakyRelu", ["X"], ["Y"], domain="ai.graphcore", **attributes
16 )
17 relu = helper.make_node("Relu", ["Y"], ["Z"])
18
19 graph = helper.make_graph(
20 [leaky_relu, relu],
21 "custom_op_test",
22 [
23 helper.make_tensor_value_info("X", TensorProto.FLOAT, (8, 8)),
24 ],
25 [
26 helper.make_tensor_value_info("Z", TensorProto.FLOAT, (8, 8)),
27 ],
28 )
29 opset_imports = [helper.make_opsetid("", 11)]
30 model = helper.make_model(graph, opset_imports=opset_imports)
31 model.opset_import.append(onnx.helper.make_opsetid("ai.graphcore", 1))
32 return model
33
34
35if __name__ == '__main__':
36 parser = argparse.ArgumentParser(
37 description='Convert ONNX model and run it on the IPU.'
38 )
39 parser.add_argument(
40 '--output_dir',
41 type=str,
42 default='./',
43 help="Full path of the directory the ONNX model will be saved to.",
44 )
45 args = parser.parse_args()
46
47 if not os.path.isdir(args.output_dir):
48 raise ValueError("--output_dir should be an existing folder")
49
50 model_path = os.path.join(args.output_dir, 'custom_op_test.onnx')
51
52 model = create_onnx_model_with_custom_op()
53 onnx.save(model, model_path)
54
55 # Convert and Run
56 compile_cmd = "bash build.sh"
57 os.system(compile_cmd)
58 abs_path = os.path.abspath(os.path.dirname(__file__))
59 run_cmd = rf"""poprt \
60--input_model {model_path} \
61--custom_shape_inference {abs_path}/custom_shape_inference.py \
62--custom_library_so_paths {abs_path}/custom_ops.so \
63--run"""
64 os.system(run_cmd)
65 # 2022-12-30 07:01:54,408 INFO cli.py:446] Bs: 8
66 # 2022-12-30 07:01:54,408 INFO cli.py:449] Latency: 0.23ms
67 # 2022-12-30 07:01:54,408 INFO cli.py:450] Tput: 35469
5.8.2. Using custom operators in PopRT
The library files of custom operators can be dynamically linked with the PopRT command line option --custom_library_so_paths
, and the shape-inference for the custom operator can be registered with --custom_shape_inference
.
The ONNX model file generated above can be executed with the following command:
poprt \
--input_model custom_op_test.onnx \
--custom_library_so_paths custom_ops.so \
--custom_shape_inference custom_shape_inference.py \
--run