This guide contains all necessary information that you need to start implementing nGraph transformations.
Before creating a transformation, do the following:
Transformation library is independent from Inference Engine target library named as inference_engine_transformations
and is located in the inference-engine/src/transformations
directory.
Transformations root directory contains two folders:
ngraph_ops
- Contains legacy opset operations needed for nGraph to CNNNetwork conversion. NOTE: This operation is prohibited inside new plugins until they are not moved to a separate directory with allowed operations.
transformations
- Includes all transformations, utils, runtime info attributes, and pass managers. NOTE: Do not use transformation that belongs to
ngraph::pass::ConvertOpSet1ToLegacy
transformations until they are not moved to a separate directory with allowed transformations.
Transformation flow in the transformation library has several layers:
ngraph::Function
.To decide where to store your transformation code, please follow these rules:
After you decide where to store your transformation code, you can start developing your own nGraph transformation.
An nGraph function is a simple thing: it stores shared pointers to ngraph::op::Result
and ngraph::op::Parameter
operations that are inputs and outputs of the graph. All other operations hold each other via shared pointers: child operation holds its parent (hard link). If the operation has no consumers and it is not a Result operation (shared pointer counter is zero), it is destructed and is not accessible anymore. Each operation in ngraph::Function
has a std::shared_ptr<ngraph::Node>
type.
For examples of how to build an nGraph function, see the Build nGraph Function page.
nGraph has three main transformation types:
ngraph::pass::FunctionPass
- straightforward way to work with ngraph::Function
directlyngraph::pass::MatcherPass
- pattern-based transformation approachngraph::pass::GraphRewrite
- container for matcher passes
ngraph::pass::FunctionPass
is used for transformations that take entire ngraph::Function
as an input and process it.
Template for FunctionPass transformation class
Using ngraph::FunctionPass
, you need to override the run_on_function
method where you will write the transformation code. Return value is true
if the original function has changed during transformation (new operation was added, or operations replacement was made, or node attributes were changed); otherwise, it is false
. For transformation API, please follow the working with ngraph::Function section. Also ngraph::FunctionPass
based transformations can be executed via pass::Manager
. See the examples in the Using pass manager section.
ngraph::pass::MatcherPass
is used for pattern-based transformations.
Template for MatcherPass transformation class
To use ngraph::pass::MatcherPass
, you need to complete these steps:
So let's go through each of these steps.
Pattern is a single root ngraph::Function
. But the only difference is that you do not need to create a function object, you just need to create and connect nGraph or special pattern operations. Then you need to take the last created operation and put it as a root of the pattern. This root node will be used as a root node in pattern matching.
NOTE: Any nodes in a pattern that have no consumers and are not registered as root will not be used in pattern matching.
The Parameter
operation in the example above has type and shape specified. These attributes are needed only to create Parameter operation class and will not be used in pattern matching.
For instructions on how to match a pattern where ShapeOf
takes any operation as an input, follow the pattern matching section.
Callback is an action applied to every pattern entrance. In general, callback is the lambda function that takes Matcher object with detected subgraph.
The example above shows the callback structure and how Matcher can be used for accessing nodes detected by pattern. Callback return value is true
if root node was replaced and another pattern cannot be applied to the same root node; otherwise, it is false
.
NOTE: It is not recommended to manipulate with nodes that are under root node. This may affect GraphRewrite execution as it is expected that all nodes that come after root node in topological order are valid and can be used in pattern matching.
MatcherPass also provides functionality that allows reporting of the newly created nodes that can be used in additional pattern matching. If MatcherPass was registered in pass::Manager
or pass::GraphRewrite
, these registered nodes will be added for additional pattern matching. That means that matcher passes registered in pass::GraphRewrite
will be applied to these nodes.
The example below shows how single MatcherPass can fuse sequence of operations using the register_new_node
method.
NOTE: If you register multiple nodes, please add them in topological order. We do not topologically sort these nodes as it is a time-consuming operation.
The last step is to register Matcher and callback inside the MatcherPass pass. To do this, call the register_matcher
method.
NOTE: Only one matcher can be registered for a single MatcherPass class.
MatcherPass has multiple ways to be executed:
ngraph::Function
using GraphRewrite - this approach gives ability to run MatcherPass on whole ngraph::Function
. Moreover, multiple MatcherPass transformation can be registered in a single GraphRewite to be executed in a single graph traversal. ngraph::Function
using pass::Manager
- this approach helps you to register MatcherPass for execution on ngraph::Function
as another transformation types. GraphRewrite pass serves for running multiple matcher passes on ngraph::Function
in a single graph traversal. Example:
In addition, GraphRewrite handles nodes that were registered by MatcherPasses during their execution. This nodes will be added to the beginning of the sequence with nodes for pattern matching.
NOTE: when using
pass::Manager
temporary GraphRewrite is used to execute single MatcherPass.
GraphRewrite has two algorithms for MatcherPasses execution. First algorithm is straightforward. It applies each MatcherPass in registration order to current node.
But it is not really efficient when you have a lot of registered passes. So first of all GraphRewrite checks that all MatcherPass patterns has type-based root node (it means that type of this node is not hidden into predicate). And then creates map from registered MatcherPasses. That helps to avoid additional cost of applying each MatcherPass for each node.
Sometimes patterns cannot be expressed via regular nGraph operations or it is too complicated. For example, if you want to detect Convolution->Add sub-graph without specifying particular input type for Convolution operation or you want to create a pattern where some of operations can have different types. And for these cases nGraph provides additional helpers to construct patterns for GraphRewrite transformations.
There are two main helpers:
ngraph::pattern::any_input
- helps to express inputs if their types are undefined.ngraph::pattern::wrap_type<T>
- helps to express nodes of pattern without specifying node attributes.Let's go through the example to have better understanding of how it works:
NOTE: Node attributes do not participate in pattern matching and are needed only for operations creation. Only operation types participate in pattern matching.
The example below shows basic usage of pattern::any_input
. Here we construct Multiply pattern with arbitrary first input and Constant as a second input. Also as Multiply is commutative operation, it does not matter in which order we set inputs (any_input/Constant or Constant/any_input) because both cases will be matched.
This example shows how we can construct a pattern when operation has arbitrary number of inputs.
This example shows how to use predicate to construct a pattern. Also it shows how to match pattern manually on given node.
NOTE: Be careful with manual matching because Matcher object holds matched nodes. To clear a match, use the m->clear_state() method.
In this chapter we will review nGraph API that allows us to manipulate with ngraph::Function
.
First of all let's talk about ngraph::Node
input/output ports. Each nGraph operation has input and output ports except cases when operation has Result
, Parameter
, or Constant
type.
Every port belongs to its node, so using a port we can access parent node, get shape and type for particular input/output, get all consumers in case of output port, and get producer node in case of input port. With output port we can set inputs for newly created operations.
Lets look at the code example.
You may notice that we usually construct operations in this way:
In this example, the opset3::Multiply
operation takes Output<Node>
and std::shared_ptr<Node>
as inputs. But the constructor takes both as Output<Node>
. In this case, std::shared_ptr<Node>
will be automatically converted to Output<Node>
if node has exactly one output port; otherwise, conversion raises an exception.
nGraph provides two ways for node replacement: via nGraph helper function and directly via port methods. We are going to review both of them.
Let's start with nGraph helper functions. The most popular function is ngraph::replace_node(old_node, new_node)
.
We will review real replacement case where Negative operation is replaced with Multiply.
ngraph::replace_node
has a constraint that number of output ports for both of ops must be the same; otherwise, it raises an exception.
The alternative way to do the same replacement is the following:
Another transformation example is insertion.
The alternative way to the insert operation is to make a node copy and use replace_node
:
Another type of node replacement is its elimination.
To eliminate operation, nGraph has special method that considers all limitations related to InferenceEngine.
replace_output_update_name
in case of successful replacement it automatically preserves friendly name and runtime info.
When developing a transformation, you need to follow these transformation rules:
Use the latest version of OpSet in your transformation. An exception is ConvertOpSetXToOpSetY transformations, where you must use operations from OpSetX and OpSetY.
nGraph has two types for shape representation: ngraph::Shape
- represents static shape. ngraph::PartialShape
- represents dynamic shape. It means that rank or some of dimensions are dynamic (undefined). ngraph::PartialShape
can be converted to ngraph::Shape
using the get_shape()
method if all dimensions are static; otherwise, conversion raises an exception.
But in most cases before getting static shape using get_shape()
method, you need to check that shape is static.
Also if your transformation requires only input shape rank or particular dimension value, please do not use the get_shape()
method. See the example below demonstrating how to avoid using get_shape()
Not using get_shape()
method makes your transformation more flexible and applicable for more cases.
Each ngraph::Node
has a unique name (used for nGraph internals) and a friendly name. In transformations we care only about friendly name because it represents the name from intermediate representation (IR). Also friendly name is used as output tensor name (until we do not have other way to represent output tensor name) and user code that requests intermediate outputs based on these names. To avoid loosing friendly name when replacing node with other node or subgraph, set the original friendly name to the latest node in replacing subgraph. See the example below.
In more advanced cases, when replaced operation has several outputs and we add additional consumers to its outputs, we make a decision how to set friendly name by arrangement.
Runtime info is a map std::map<std::string, std::shared_ptr<Variant>>
located inside ngraph::Node
class. It represents additional attributes in ngraph::Node
. These attributes can be set by users or by plugins and when executing transformation that changes ngraph::Function
we need to preserve these attributes as they will not be automatically propagated. In most cases, transformations have the following types: 1:1 (replace node with another node), 1:N (replace node with a sub-graph), N:1 (fuse sub-graph into a single node), N:M (any other transformation). Currently, there is no mechanism that automatically detects transformation types, so we need to propagate this runtime information manually. See the examples below.
When transformation has multiple fusions or decompositions, ngraph::copy_runtime_info
must be called multiple times for each case.
Note: copy_runtime_info removes rt_info from destination nodes. If you want to keep it, you need to specify them in source nodes like this: copy_runtime_info({a, b, c}, {a, b})
If your transformation inserts constant sub-graphs that need to be folded, do not forget to use ngraph::pass::ConstantFolding()
after your transformation or call constant folding directly for operation. The example below shows how constant subgraph can be constructed.
Manual constant folding is more preferable than ngraph::pass::ConstantFolding()
because it is much faster.
Below you can find an example of manual constant folding:
In transformation development process:
NGRAPH_DEPRECATED
macros in its definition.shared_ptr<Node>
as an input for other node if type of node is unknown or it has multiple outputs. Use explicit output port.validate_nodes_and_infer_types
call for ngraph::Function
. If you are using pass::Manager
, it will automatically call this method after each transformation execution.ngraph::ConstantFolding
pass if your transformation creates constant subgraphs.ngraph::pass::MatcherPass
, do not change nodes that come after the root node in topological order.ngraph::pass::Manager
is a container class that can store the list of transformations and execute them. The main idea of this class is to have high-level representation for grouped list of transformations. It can register and apply any transformation types on function. In addition, ngraph::pass::Manager
has extended debug capabilities (find more information in the how to debug transformations section).
The example below shows basic usage of ngraph::pass::Manager
Another example shows how multiple matcher passes can be united into single GraphRewrite.
Note: nGraph used to have the
pass::PassConfig
class for transformation pipeline manipulation.
This mechanism is now obsolete and the pass::PassConfig
class will be removed in future release.
The most popular tool for transformations debugging is the ngraph::pass::VisualizeTree
transformation, which visualizes ngraph::Function.
Usage example:
ngraph::pass::VisualizeTree
can be parametrized via environment variables:
Note: current VisualTree does not have user-friendly interface and it will be changed in the nearest future. The intention is to move visualization abilities inside transformations.
If you are using ngraph::pass::Manager
to run sequence of transformations, you can get additional debug capabilities by using the following environment variables:
Note: Make sure that you have dot installed on your machine; otherwise, it will silently save only dot file without svg file.
This topic is mostly related to conversion to legacy opset and plugins that are based on CNNNetwork. But this mechanism still can be applied for other cases. Let's suppose that plugin X enabled the opset3::StridedSlice
operation support and you want to disable the ngraph::pass::ConvertStridedSliceToCrop
transformation for plugin X. To do this, you need to create a callback on plugin side and pass it to transformation. And also you need to update particular transformation to use this callback.
If you are developing new transformation inside plugin, you need to add test into the template_plugin/tests/functional/transformations
folder. We have two types of tests: nGraph reader tests located in inference-engine/tests/functional/inference_engine/ngraph_reader
and transformation tests located in inference-engine/tests/functional/inference_engine/transformations
Reader tests are IR based and test end-to-end conversion from IR to CNNNetwork. Transformation tests test single ngraph transformations or low-level functions that are used inside transformations.
The basic transformation test looks like this: