C++ Fusion Pass Development Guide
This guide is for developers who want to write GE fusion passes in C++. It is recommended to first read the language-independent mechanism description: Fusion Pattern Pass Mechanism.
C++ passes are delivered as dynamic libraries. Developers implement a pass class, register it with GE, and compile it into a .so. When GE compiles a model, it loads the .so and executes the pass at a specified stage.
If you are still exploring patterns, it is recommended to use the Python Fusion Pass Development Guide for quick validation; migrate to C++ once the pattern is stable.
1. Which Pass to Choose
| Goal | Recommended Interface |
|---|---|
| Match a fixed topology and replace it with another topology | PatternFusionPass |
| Match a specific operator type and decompose it into multiple operators | DecomposePass |
This guide covers PatternFusionPass first, then DecomposePass.
2. Minimal Example: Delete Add(x, 0)
Goal:
x ----\
Add ---- out ==> x ---- out
0 ----/
The core C++ pass code consists of four parts:
- Inherit
PatternFusionPass. Patterns()defines the structure to match.MeetRequirements()checks if the constant is 0.Replacement()returns the replacement structure.
#include <cmath>
#include <cstdint>
#include <iostream>
#include "es_all_ops.h"
#include "ge/fusion/pass/pattern_fusion_pass.h"
using namespace ge;
using namespace ge::fusion;
class AddZeroPass : public PatternFusionPass {
protected:
std::vector<PatternUniqPtr> Patterns() override {
std::vector<PatternUniqPtr> patterns;
auto builder = es::EsGraphBuilder("add_zero_pattern");
auto x = builder.CreateInput(0);
auto zero = es::Const(builder);
auto add = es::Add(x, zero);
auto graph = builder.BuildAndReset({add});
patterns.emplace_back(std::make_unique<Pattern>(std::move(*graph)));
return patterns;
}
bool MeetRequirements(const std::unique_ptr<MatchResult> &match_result) override {
for (const auto &node : match_result->GetMatchedNodes()) {
AscendString type;
node.GetType(type);
if (type != "Const") {
continue;
}
Tensor value;
if (node.GetAttr("value", value) != GRAPH_SUCCESS) {
return false;
}
return IsZero(value);
}
return false;
}
GraphUniqPtr Replacement(const std::unique_ptr<MatchResult> &match_result) override {
auto builder = es::EsGraphBuilder("add_zero_replacement");
auto x = builder.CreateInput(0);
return builder.BuildAndReset({x});
}
private:
bool IsZero(const Tensor &tensor) const {
switch (tensor.GetTensorDesc().GetDataType()) {
case DT_FLOAT:
return std::fabs(*reinterpret_cast<const float *>(tensor.GetData())) < 1e-6;
case DT_DOUBLE:
return std::fabs(*reinterpret_cast<const double *>(tensor.GetData())) < 1e-15;
case DT_INT32:
return *reinterpret_cast<const int32_t *>(tensor.GetData()) == 0;
default:
return false;
}
}
};
REG_FUSION_PASS(AddZeroPass).Stage(CustomPassStage::kBeforeInferShape);
A complete runnable example is available at AddZeroPass C++ Example.
3. Patterns: Define What to Find
Patterns() returns one or more patterns. Each pattern is a small graph.
std::vector<PatternUniqPtr> Patterns() override {
std::vector<PatternUniqPtr> patterns;
auto builder = es::EsGraphBuilder("pattern");
auto a = builder.CreateInput(0);
auto b = builder.CreateInput(1);
auto c = builder.CreateInput(2);
auto matmul = es::MatMul(a, b);
auto add = es::Add(matmul, c);
auto graph = builder.BuildAndReset({add});
patterns.emplace_back(std::make_unique<Pattern>(std::move(*graph)));
return patterns;
}
This pattern represents:
a ----\
MatMul ----\
b ----/ Add ---- pattern output
c ----------------/
To support both MatMul + Add and BatchMatMulV2 + Add, create two patterns and add both to patterns.
When writing patterns, note:
- External inputs are declared with
CreateInput. - Tensors that will still be used externally after replacement must be outputs of the pattern.
- Input count for normal operators must match the real graph.
- Do not use control edges, subgraphs, or nodes with dynamic input/output counts in patterns.
4. MeetRequirements: Determine Whether to Replace
Patterns() only handles topology matching. If additional checks are needed after topology matching, write MeetRequirements().
For example, after matching Add(x, Const), verify that Const equals 0:
bool MeetRequirements(const std::unique_ptr<MatchResult> &match_result) override {
for (const auto &node : match_result->GetMatchedNodes()) {
AscendString type;
node.GetType(type);
if (type != "Const") {
continue;
}
Tensor value;
if (node.GetAttr("value", value) != GRAPH_SUCCESS) {
return false;
}
return IsZero(value);
}
return false;
}
If no filtering is needed, this method can be omitted; it returns true by default.
5. Replacement: Define What to Replace With
Replacement() returns the replacement graph.
When deleting Add(x, 0), the replacement graph has only one external input:
GraphUniqPtr Replacement(const std::unique_ptr<MatchResult> &match_result) override {
auto builder = es::EsGraphBuilder("replacement");
auto x = builder.CreateInput(0);
return builder.BuildAndReset({x});
}
When fusing MatMul + Add into GEMM:
GraphUniqPtr Replacement(const std::unique_ptr<MatchResult> &match_result) override {
auto builder = es::EsGraphBuilder("replacement");
auto a = builder.CreateInput(0);
auto b = builder.CreateInput(1);
auto c = builder.CreateInput(2);
auto alpha = builder.CreateScalar(1);
auto beta = builder.CreateScalar(1);
auto gemm = es::GEMM(a, b, c, alpha, beta);
return builder.BuildAndReset({gemm});
}
If the pass is registered after InferShape, shape information for new nodes in the replacement needs to be handled manually. Refer to existing examples for using InferShapeUtil::InferShape when shape inference is needed for replacement.
6. CaptureTensor: Read Key Tensors in Pattern
When MeetRequirements() or Replacement() needs to know which real node corresponds to a intermediate tensor, capture it in the pattern.
auto matmul = es::MatMul(a, b);
auto add = es::Add(matmul, c);
auto graph = builder.BuildAndReset({add});
auto pattern = std::make_unique<Pattern>(std::move(*graph));
pattern->CaptureTensor({*matmul.GetProducer(), 0});
patterns.emplace_back(std::move(pattern));
After successful matching, retrieve from match_result:
NodeIo matmul_output;
if (match_result->GetCapturedTensor(0, matmul_output) != GRAPH_SUCCESS) {
return false;
}
Refer to capture tensor C++ example.
7. PatternMatcherConfig: Put Simple Conditions in Matcher
If you want the matcher to directly check Const values or IR attributes, pass configuration to the PatternFusionPass constructor.
class MatmulAddFusionPass : public PatternFusionPass {
public:
MatmulAddFusionPass()
: PatternFusionPass(PatternMatcherConfigBuilder()
.EnableConstValueMatch()
.EnableIrAttrMatch()
.Build()) {}
};
Common configurations:
| Configuration | Effect |
|---|---|
EnableConstValueMatch() |
Const values in pattern must match Const values in real graph |
EnableIrAttrMatch() |
IR attributes and values in pattern must match real graph |
If judgment requires floating-point tolerance, dtype normalization, or more complex logic, it is still recommended to put it in MeetRequirements().
Refer to PatternMatcherConfig C++ example.
8. Register Execution Stage
Use REG_FUSION_PASS to register PatternFusionPass:
REG_FUSION_PASS(AddZeroPass).Stage(CustomPassStage::kBeforeInferShape);
Common stages:
| C++ Enumeration | Usage Recommendation |
|---|---|
CustomPassStage::kBeforeInferShape |
Most commonly used. Replacement will go through unified shape inference |
CustomPassStage::kAfterInferShape |
Use when dependent on inferred shape; replacement must ensure shape information |
CustomPassStage::kAfterBuiltinFusionPass |
Execute after GE built-in fusion |
CustomPassStage::kAfterOriginGraphOptimize |
Execute after original graph optimization |
For initial development, use kBeforeInferShape.
9. Writing DecomposePass
If you want to decompose one node into multiple nodes, use DecomposePass.
Skeleton is as follows:
#include "ge/fusion/pass/decompose_pass.h"
#include "es_all_ops.h"
using namespace ge;
using namespace ge::fusion;
class MyDecomposePass : public DecomposePass {
public:
explicit MyDecomposePass(const std::vector<AscendString> &op_types)
: DecomposePass(op_types) {}
protected:
bool MeetRequirements(const GNode &matched_node) override {
// Read matched_node attributes to determine if decomposition is needed
return true;
}
GraphUniqPtr Replacement(const GNode &matched_node) override {
auto builder = es::EsGraphBuilder("replacement");
// Construct subgraph for replacing matched_node
...
return builder.BuildAndReset({output});
}
};
REG_DECOMPOSE_PASS(MyDecomposePass, {"Conv2D"}).Stage(CustomPassStage::kAfterInferShape);
The second parameter of REG_DECOMPOSE_PASS is the list of operator types to match. GE will pass real nodes of these types to the pass, then MeetRequirements() makes further judgment.
Complete example see DecomposePass C++ example.
10. Compilation and Running
Each example directory comes with CMakeLists.txt. General process is as follows.
Set CANN environment variables:
source ${ASCEND_PATH}/set_env.sh
Compile and install pass dynamic library:
mkdir build
cd build
cmake ..
make -j$(nproc) <target_name>
make install
CMake configuration not expanded in this document, use examples as template during development:
If need to add new header file paths or link libraries, append in corresponding positions of example CMakeLists.txt, do not delete original configuration.
Offline compilation can use atc to trigger:
atc --model=./model.onnx --framework=5 --soc_version=xxx --output=./model
Online scenario usually triggers GE compilation through torch_forward.py in examples.
11. Verification and Troubleshooting
Recommend enabling graph dump:
export DUMP_GE_GRAPH=1
Compare graphs before and after pass:
PreRunBegin: Before pass execution.RunCustomPass...: After custom pass execution.
Common problems:
| Phenomenon | Possible Cause | Check Method |
|---|---|---|
| pass not executed | .so not installed to directory GE will load, or registration stage incorrect |
Check installation path and registration macro |
| pattern not matched | Operator type, input count, output boundary inconsistent | Compare dump graph and Patterns() |
| matched but not replaced | MeetRequirements() returned false |
Print matched node attributes |
| Graph abnormal after replacement | replacement output did not cover Tensor needed by external consumers | Go back to mechanism document boundary rules |
When more logs needed, can set:
export ASCEND_SLOG_PRINT_TO_STDOUT=1
export ASCEND_GLOBAL_LOG_LEVEL=0
When using atc, can add --log=debug.