1062 lines
37 KiB
C++
1062 lines
37 KiB
C++
// Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include "paddle/fluid/pir/transforms/sub_graph_detector.h"
|
|
|
|
#include <memory>
|
|
|
|
#include <climits>
|
|
#include <iterator>
|
|
#include <queue>
|
|
#include <regex>
|
|
#include <set>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
|
|
#ifdef PADDLE_WITH_CINN
|
|
#include "paddle/cinn/hlir/dialect/operator/ir/manual_op.h"
|
|
#include "paddle/cinn/hlir/dialect/operator/ir/op_dialect.h"
|
|
#include "paddle/cinn/hlir/framework/pir/utils.h"
|
|
#include "paddle/cinn/utils/string.h"
|
|
#endif
|
|
|
|
#include "paddle/fluid/pir/dialect/operator/ir/pd_op.h"
|
|
#include "paddle/fluid/pir/dialect/operator/trait/inplace.h"
|
|
#include "paddle/fluid/pir/utils/general_functions.h"
|
|
#include "paddle/pir/include/core/builder.h"
|
|
#include "paddle/pir/include/core/builtin_op.h"
|
|
#include "paddle/pir/include/core/op_trait.h"
|
|
#include "paddle/pir/include/dialect/control_flow/ir/cf_dialect.h"
|
|
#include "paddle/pir/include/dialect/control_flow/ir/cf_op.h"
|
|
#include "paddle/pir/include/pass/pass.h"
|
|
#include "paddle/pir/include/pass/pass_registry.h"
|
|
|
|
#include "paddle/common/flags.h"
|
|
#include "paddle/common/macros.h"
|
|
|
|
#ifdef PADDLE_WITH_DNNL
|
|
#include "paddle/fluid/pir/dialect/operator/ir/onednn_op.h"
|
|
#include "paddle/fluid/pir/dialect/operator/ir/op_onednn_dialect.h"
|
|
#include "paddle/fluid/pir/dialect/operator/trait/onednn.h"
|
|
#endif
|
|
|
|
COMMON_DECLARE_bool(merge_all_horizontal_groups);
|
|
REGISTER_FILE_SYMBOLS(sub_graph_detector);
|
|
namespace pir {
|
|
std::vector<Operation*> InverselyTopologicalSort(Block* block) {
|
|
std::vector<Operation*> sort_ops;
|
|
std::unordered_map<Operation*, size_t> pending_count;
|
|
// step 1: initialize pending_cout for defined op
|
|
for (auto& op : *block) {
|
|
if (pending_count.find(&op) == pending_count.end()) {
|
|
pending_count[&op] = 0;
|
|
}
|
|
for (auto operand : GetUsedExternalValue(op)) {
|
|
if (!operand || !operand.defining_op()) {
|
|
continue;
|
|
}
|
|
auto* defined_op = operand.defining_op();
|
|
if (pending_count.find(defined_op) != pending_count.end()) {
|
|
++pending_count[defined_op];
|
|
} else {
|
|
pending_count[defined_op] = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::queue<Operation*> queue;
|
|
for (auto& op : *block) {
|
|
if (pending_count[&op] == 0) {
|
|
queue.push(&op);
|
|
}
|
|
}
|
|
|
|
while (!queue.empty()) {
|
|
auto* op = queue.front();
|
|
queue.pop();
|
|
sort_ops.push_back(op);
|
|
for (auto operand : GetUsedExternalValue(*op)) {
|
|
if (!operand || !operand.defining_op()) {
|
|
continue;
|
|
}
|
|
auto* defined_op = operand.defining_op();
|
|
--pending_count[defined_op];
|
|
if (defined_op && pending_count[defined_op] == 0 &&
|
|
defined_op->GetParent() == block) {
|
|
queue.push(defined_op);
|
|
}
|
|
}
|
|
}
|
|
|
|
PADDLE_ENFORCE_EQ(
|
|
block->size(),
|
|
sort_ops.size(),
|
|
common::errors::InvalidArgument("sort_ops.size() must be equal to "
|
|
"block.size(), but received %d != %d",
|
|
block->size(),
|
|
sort_ops.size()));
|
|
|
|
return sort_ops;
|
|
}
|
|
|
|
std::vector<Operation*> GetProducerOpsReverseSort(
|
|
Operation* op, const std::unordered_map<Operation*, int>& op2index) {
|
|
std::unordered_set<Operation*> producers;
|
|
|
|
std::vector<Operation*> vec_res;
|
|
for (auto operand : GetUsedExternalValue(*op)) {
|
|
if (!operand || !operand.defining_op()) {
|
|
continue;
|
|
}
|
|
auto* source_op = operand.defining_op();
|
|
if (source_op && !producers.count(source_op) &&
|
|
source_op->GetParent() == op->GetParent()) {
|
|
producers.insert(source_op);
|
|
PADDLE_ENFORCE(
|
|
op2index.count(source_op),
|
|
common::errors::PreconditionNotMet("source op MUST in op2index map"));
|
|
vec_res.emplace_back(source_op);
|
|
}
|
|
}
|
|
|
|
std::sort(
|
|
vec_res.begin(), vec_res.end(), [&op2index](Operation* a, Operation* b) {
|
|
return op2index.at(a) > op2index.at(b);
|
|
});
|
|
|
|
return vec_res;
|
|
}
|
|
|
|
std::vector<Operation*> GetProducerOps(Operation* op) {
|
|
std::vector<Operation*> producers;
|
|
|
|
for (auto operand : GetUsedExternalValue(*op)) {
|
|
if (!operand || !operand.defining_op()) {
|
|
continue;
|
|
}
|
|
auto* source_op = operand.defining_op();
|
|
if (source_op && source_op->GetParent() == op->GetParent()) {
|
|
producers.push_back(source_op);
|
|
}
|
|
}
|
|
return producers;
|
|
}
|
|
|
|
std::vector<Operation*> GetConsumerOps(
|
|
Operation* op, const std::unordered_map<Operation*, int>& op2index) {
|
|
std::vector<Operation*> consumers;
|
|
|
|
for (auto& result : op->results()) {
|
|
for (auto it = result.use_begin(); it != result.use_end(); ++it) {
|
|
auto parent_op = it->owner();
|
|
while (parent_op) {
|
|
if (op2index.count(parent_op)) {
|
|
consumers.push_back(parent_op);
|
|
break;
|
|
}
|
|
parent_op = parent_op->GetParentOp();
|
|
}
|
|
}
|
|
}
|
|
return consumers;
|
|
}
|
|
|
|
std::vector<std::pair<Value, Value>> GetInplaceValues(Operation* op) {
|
|
if (!op->HasInterface<paddle::dialect::OpYamlInfoInterface>()) return {};
|
|
auto op_info =
|
|
op->dyn_cast<paddle::dialect::OpYamlInfoInterface>().GetOpInfo();
|
|
auto input_info_list = std::get<0>(op_info);
|
|
auto output_info_list = std::get<2>(op_info);
|
|
auto inplace_info_map = std::get<3>(op_info).inplace;
|
|
std::unordered_map<std::string, Value> input_name_value;
|
|
std::unordered_map<std::string, Value> output_name_value;
|
|
for (size_t i = 0; i < input_info_list.size(); ++i) {
|
|
input_name_value[input_info_list[i].name] = op->operand_source(i);
|
|
}
|
|
for (size_t i = 0; i < output_info_list.size(); ++i) {
|
|
output_name_value[output_info_list[i].name] = op->result(i);
|
|
}
|
|
std::vector<std::pair<Value, Value>> inplace_values;
|
|
for (const auto& [out, in] : inplace_info_map) {
|
|
inplace_values.emplace_back(output_name_value[out], input_name_value[in]);
|
|
}
|
|
return inplace_values;
|
|
}
|
|
|
|
bool IsSideEffectButNotInplaceOp(Operation* op) {
|
|
return op->HasTrait<SideEffectTrait>() &&
|
|
!op->HasTrait<paddle::dialect::InplaceTrait>();
|
|
}
|
|
|
|
static std::string OpsDebugStr(std::vector<Operation*> ops) {
|
|
std::stringstream ss;
|
|
IrPrinter printer(ss);
|
|
for (const auto* op : ops) {
|
|
printer.PrintOperation(*op);
|
|
ss << "{" << op->id() << "}\n";
|
|
}
|
|
return ss.str();
|
|
}
|
|
|
|
struct SubGraph : public std::enable_shared_from_this<SubGraph> {
|
|
using SubGraphPtr = std::shared_ptr<SubGraph>;
|
|
SubGraph() = delete;
|
|
SubGraph(Operation* op, int index, bool subst)
|
|
: substitute(subst), topo_index(index), id(UniqueId()) {
|
|
ops.push_back(op);
|
|
}
|
|
|
|
void Merge(const SubGraphPtr& other);
|
|
|
|
static size_t UniqueId() {
|
|
static std::atomic<size_t> counter{0};
|
|
return counter++;
|
|
}
|
|
|
|
template <typename V>
|
|
static std::string JointName(const V& subgraphs) {
|
|
std::stringstream ss;
|
|
for (const auto& subgraph : subgraphs) {
|
|
ss << subgraph->name() << ", ";
|
|
}
|
|
auto str = ss.str();
|
|
return str.empty() ? str : str.substr(0, str.size() - 2);
|
|
}
|
|
|
|
std::string DebugStr(bool print_ops = false) const {
|
|
std::stringstream ss;
|
|
ss << "=========================================\n";
|
|
ss << name() << " (substitute=" << substitute << ", "
|
|
<< "index=" << topo_index << ", "
|
|
<< "size=" << ops.size() << ")\n";
|
|
if (print_ops) ss << OpsDebugStr(ops);
|
|
ss << "upstream: " << JointName(upstreams);
|
|
ss << "\ndownstream: " << JointName(downstreams);
|
|
return ss.str();
|
|
}
|
|
|
|
std::string name() const {
|
|
return std::string("Subgraph_") + std::to_string(id);
|
|
}
|
|
|
|
struct CompareById {
|
|
bool operator()(const SubGraphPtr& lhs, const SubGraphPtr& rhs) const {
|
|
// sort by reverse order of topo id
|
|
return lhs->id > rhs->id;
|
|
}
|
|
};
|
|
|
|
struct CompareByTopo {
|
|
bool operator()(const SubGraphPtr& lhs, const SubGraphPtr& rhs) const {
|
|
// sort by topo index
|
|
return lhs->topo_index > rhs->topo_index;
|
|
}
|
|
};
|
|
|
|
std::vector<Operation*> ops;
|
|
std::set<SubGraphPtr, CompareById> upstreams;
|
|
std::set<SubGraphPtr, CompareById> downstreams;
|
|
|
|
bool substitute; // whether this subgraph can be merged
|
|
int topo_index;
|
|
size_t id;
|
|
};
|
|
using SubGraphPtr = std::shared_ptr<SubGraph>;
|
|
|
|
void SubGraph::Merge(const SubGraphPtr& other) {
|
|
// Merge other subgraph into this subgraph:
|
|
// Inherit its upstreams/downstreams and ops
|
|
SubGraphPtr self = shared_from_this();
|
|
for (const auto& upstream : other->upstreams) {
|
|
if (upstream == self) continue;
|
|
upstream->downstreams.erase(other);
|
|
upstream->downstreams.insert(self);
|
|
upstreams.insert(upstream);
|
|
}
|
|
for (const auto& downstream : other->downstreams) {
|
|
if (downstream == self) continue;
|
|
downstream->upstreams.erase(other);
|
|
downstream->upstreams.insert(self);
|
|
downstreams.insert(downstream);
|
|
}
|
|
upstreams.erase(other);
|
|
downstreams.erase(other);
|
|
ops.insert(ops.begin(), other->ops.begin(), other->ops.end());
|
|
}
|
|
|
|
bool HasSinkRoute(const SubGraphPtr& source, const SubGraphPtr& target) {
|
|
if (source == target) return true;
|
|
std::unordered_set<SubGraphPtr> visited;
|
|
std::queue<SubGraphPtr> queue;
|
|
queue.push(source);
|
|
visited.insert(source);
|
|
while (!queue.empty()) {
|
|
SubGraphPtr cur = queue.front();
|
|
queue.pop();
|
|
if (cur == target) return true;
|
|
if (cur->topo_index > target->topo_index) continue;
|
|
for (const auto& subgraph : cur->downstreams) {
|
|
if (visited.count(subgraph)) continue;
|
|
queue.push(subgraph);
|
|
visited.insert(subgraph);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool HasLiftRoute(const SubGraphPtr& source, const SubGraphPtr& target) {
|
|
if (source == target) return true;
|
|
std::unordered_set<SubGraphPtr> visited;
|
|
std::queue<SubGraphPtr> queue;
|
|
queue.push(source);
|
|
visited.insert(source);
|
|
while (!queue.empty()) {
|
|
SubGraphPtr cur = queue.front();
|
|
queue.pop();
|
|
if (cur == target) return true;
|
|
if (source->topo_index < target->topo_index) continue;
|
|
for (const auto& subgraph : cur->upstreams) {
|
|
if (visited.count(subgraph)) continue;
|
|
queue.push(subgraph);
|
|
visited.insert(subgraph);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool HasRoute(const SubGraphPtr& up, const SubGraphPtr& down) {
|
|
return HasSinkRoute(up, down) || HasLiftRoute(down, up);
|
|
}
|
|
|
|
bool CanFuseUpstream2Downstream(const SubGraphPtr& upstream,
|
|
const SubGraphPtr& downstream) {
|
|
PADDLE_ENFORCE(upstream->downstreams.count(downstream) &&
|
|
downstream->upstreams.count(upstream),
|
|
::common::errors::InvalidArgument(
|
|
"Subgraphs to be fused must have direct relationship."));
|
|
auto up_downstreams = upstream->downstreams;
|
|
up_downstreams.erase(downstream);
|
|
auto down_upstreams = downstream->upstreams;
|
|
down_upstreams.erase(upstream);
|
|
if (up_downstreams.empty() || down_upstreams.empty()) return true;
|
|
for (const auto& subgraph : up_downstreams) {
|
|
if (HasSinkRoute(subgraph, downstream)) return false;
|
|
}
|
|
for (const auto& subgraph : down_upstreams) {
|
|
if (HasLiftRoute(subgraph, upstream)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::optional<std::string> DetectCirclesInSubgraphs(
|
|
const std::vector<SubGraphPtr>& subgraph_list) {
|
|
std::set<SubGraphPtr, SubGraph::CompareById> subgraph_set(
|
|
subgraph_list.begin(), subgraph_list.end());
|
|
std::unordered_map<SubGraphPtr, size_t> in_degree;
|
|
std::unordered_map<SubGraphPtr, size_t> out_degree;
|
|
for (const auto& subgraph : subgraph_set) {
|
|
in_degree[subgraph] = subgraph->upstreams.size();
|
|
out_degree[subgraph] = subgraph->downstreams.size();
|
|
}
|
|
// Recursively remove nodes with in_degree or out_degree = 0
|
|
bool erase_flag = true;
|
|
while (erase_flag) {
|
|
erase_flag = false;
|
|
for (const auto& subgraph : subgraph_list) {
|
|
if (subgraph_set.count(subgraph) == 0) continue;
|
|
if (in_degree[subgraph] == 0) {
|
|
for (const auto& downstream : subgraph->downstreams) {
|
|
in_degree[downstream]--;
|
|
}
|
|
subgraph_set.erase(subgraph);
|
|
erase_flag = true;
|
|
continue;
|
|
}
|
|
if (out_degree[subgraph] == 0) {
|
|
for (const auto& upstream : subgraph->upstreams) {
|
|
out_degree[upstream]--;
|
|
}
|
|
subgraph_set.erase(subgraph);
|
|
erase_flag = true;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
if (subgraph_set.empty()) return std::nullopt;
|
|
// If subgraph_set is not empty, there are circles in the subgraphs.
|
|
auto circle_size = subgraph_set.size();
|
|
std::stringstream ss;
|
|
ss << "Circles detected in subgraphs (size=" << circle_size << "): \n";
|
|
for (const auto& subgraph : subgraph_set) {
|
|
ss << subgraph->DebugStr() << "\n";
|
|
}
|
|
return std::make_optional(ss.str());
|
|
}
|
|
|
|
class SubgraphDetector {
|
|
public:
|
|
SubgraphDetector(Block* block, const OpClassifier& classifier);
|
|
|
|
void SubgraphFusion();
|
|
|
|
std::vector<GroupOpsVec> BuildGroups();
|
|
|
|
private:
|
|
void ReorderIndexOfSubgraphs();
|
|
|
|
bool MergeSource2Target(const SubGraphPtr& source, const SubGraphPtr& target);
|
|
|
|
void FallbackSubGraphFusion(const SubGraphPtr& source,
|
|
const SubGraphPtr& target,
|
|
const SubGraph& source_back,
|
|
const SubGraph& target_back);
|
|
|
|
void InitInplaceOpsOrder(const std::vector<Operation*>& inplace_ops);
|
|
|
|
bool CheckSideEffectOpsOrder() {
|
|
int last_index = INT_MIN;
|
|
for (const auto& op : side_effect_ops_) {
|
|
auto subgraph = GetOpSubgraph(op);
|
|
if (subgraph->topo_index < last_index) return false;
|
|
last_index = subgraph->topo_index;
|
|
}
|
|
for (const auto& ops : inplace_ops_order_) {
|
|
last_index = INT_MIN;
|
|
for (const auto& op : ops) {
|
|
auto subgraph = GetOpSubgraph(op);
|
|
if (subgraph->topo_index < last_index) return false;
|
|
last_index = subgraph->topo_index;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
SubGraphPtr GetOpSubgraph(Operation* op) {
|
|
PADDLE_ENFORCE(
|
|
op2subgraph_.count(op),
|
|
::common::errors::InvalidArgument(
|
|
"Can not find op in op2subgraph_: \n%s", OpsDebugStr({op})));
|
|
return op2subgraph_.at(op);
|
|
}
|
|
|
|
std::vector<SubGraphPtr> GetSubgraphList() {
|
|
std::unordered_set<SubGraphPtr> subgraph_set;
|
|
std::vector<SubGraphPtr> subgraph_list;
|
|
for (const auto& op : sort_ops_) {
|
|
SubGraphPtr subgraph = GetOpSubgraph(op);
|
|
if (subgraph_set.count(subgraph)) continue;
|
|
subgraph_set.insert(subgraph);
|
|
subgraph_list.push_back(subgraph);
|
|
}
|
|
return subgraph_list;
|
|
}
|
|
|
|
std::unordered_map<Operation*, int> op2index_;
|
|
std::vector<Operation*> sort_ops_;
|
|
std::vector<Operation*> side_effect_ops_;
|
|
std::vector<std::vector<Operation*>> inplace_ops_order_;
|
|
std::unordered_map<Operation*, SubGraphPtr> op2subgraph_;
|
|
std::unordered_set<int> subgraph_index_set_;
|
|
};
|
|
|
|
void SubgraphDetector::ReorderIndexOfSubgraphs() {
|
|
// After merging subgraphs with direct relation, brother subgraphs with
|
|
// indirect relation may not be detected by index order. So we need to
|
|
// reorder the index of subgraphs.
|
|
using SubGraphQueue = std::priority_queue<SubGraphPtr,
|
|
std::vector<SubGraphPtr>,
|
|
SubGraph::CompareByTopo>;
|
|
SubGraphQueue queue; // min heap
|
|
std::unordered_map<SubGraphPtr, int> in_degree;
|
|
for (auto it = sort_ops_.rbegin(); it != sort_ops_.rend(); ++it) {
|
|
auto subgraph = GetOpSubgraph(*it);
|
|
if (in_degree.count(subgraph)) continue;
|
|
in_degree[subgraph] = subgraph->upstreams.size();
|
|
if (in_degree[subgraph] == 0) queue.push(subgraph);
|
|
}
|
|
subgraph_index_set_.clear();
|
|
int index = 0;
|
|
while (!queue.empty()) {
|
|
auto subgraph = queue.top();
|
|
queue.pop();
|
|
subgraph->topo_index = index++;
|
|
subgraph_index_set_.insert(subgraph->topo_index);
|
|
for (const auto& downstream : subgraph->downstreams) {
|
|
in_degree[downstream]--;
|
|
if (in_degree[downstream] == 0) queue.push(downstream);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SubgraphDetector::MergeSource2Target(const SubGraphPtr& source,
|
|
const SubGraphPtr& target) {
|
|
VLOG(6) << "Merge source: " << source->DebugStr();
|
|
VLOG(6) << "Merge target: " << target->DebugStr();
|
|
SubGraph source_back = *source;
|
|
SubGraph target_back = *target;
|
|
target->Merge(source);
|
|
for (const auto& op : source->ops) {
|
|
op2subgraph_[op] = target;
|
|
}
|
|
const auto& update_topo_index = [&]() -> void {
|
|
int max_index = std::max(source->topo_index, target->topo_index);
|
|
int min_index = std::min(source->topo_index, target->topo_index);
|
|
auto merged = target;
|
|
// Check if merged subgraph and its related subgraphs
|
|
// satisfy the topological order condition.
|
|
int upstream_max_index = -1, downstream_min_index = INT_MAX;
|
|
for (const auto& upstream : merged->upstreams) {
|
|
upstream_max_index = std::max(upstream->topo_index, upstream_max_index);
|
|
}
|
|
for (const auto& downstream : merged->downstreams) {
|
|
downstream_min_index =
|
|
std::min(downstream->topo_index, downstream_min_index);
|
|
}
|
|
// 1. If satisfy the topological order after merging, just set max_index
|
|
VLOG(6) << "Check if satisfy the topological order after merging";
|
|
if (min_index > upstream_max_index && max_index < downstream_min_index) {
|
|
merged->topo_index = max_index;
|
|
subgraph_index_set_.erase(min_index);
|
|
return;
|
|
}
|
|
// 2. If not satisfy the order, find a index between upstream_max_index
|
|
// and downstream_min_index while not in subgraph_index_set_.
|
|
VLOG(6) << "Try to find a valid index not in subgraph_index_set_";
|
|
for (int i = upstream_max_index + 1; i < downstream_min_index; ++i) {
|
|
if (!subgraph_index_set_.count(i)) {
|
|
merged->topo_index = i;
|
|
subgraph_index_set_.erase(min_index);
|
|
subgraph_index_set_.erase(max_index);
|
|
subgraph_index_set_.insert(i);
|
|
return;
|
|
}
|
|
}
|
|
// 3. If can not find a valid index, reorder topo index of all subgraphs.
|
|
VLOG(6) << "Reorder topo index of all subgraphs";
|
|
merged->topo_index = max_index;
|
|
ReorderIndexOfSubgraphs();
|
|
};
|
|
update_topo_index();
|
|
if (CheckSideEffectOpsOrder()) {
|
|
VLOG(6) << "Merged subgraph: " << target->DebugStr();
|
|
return true;
|
|
} else {
|
|
FallbackSubGraphFusion(source, target, source_back, target_back);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void SubgraphDetector::FallbackSubGraphFusion(const SubGraphPtr& source,
|
|
const SubGraphPtr& target,
|
|
const SubGraph& source_back,
|
|
const SubGraph& target_back) {
|
|
const auto fall_back_subgraph = [](const SubGraphPtr& subgraph,
|
|
const SubGraph& back) {
|
|
subgraph->ops = back.ops;
|
|
subgraph->upstreams = back.upstreams;
|
|
subgraph->downstreams = back.downstreams;
|
|
subgraph->topo_index = back.topo_index;
|
|
};
|
|
// 1. Update source and target subgraph
|
|
subgraph_index_set_.erase(target->topo_index);
|
|
subgraph_index_set_.insert(source_back.topo_index);
|
|
subgraph_index_set_.insert(target_back.topo_index);
|
|
fall_back_subgraph(source, source_back);
|
|
fall_back_subgraph(target, target_back);
|
|
for (const auto& op : source->ops) {
|
|
op2subgraph_[op] = source;
|
|
}
|
|
// 2. Update source's upstreams and downstreams
|
|
for (const auto& upstream : source->upstreams) {
|
|
if (upstream == target) continue;
|
|
upstream->downstreams.insert(source);
|
|
if (target->upstreams.count(upstream)) continue;
|
|
upstream->downstreams.erase(target);
|
|
}
|
|
for (const auto& downstream : source->downstreams) {
|
|
if (downstream == target) continue;
|
|
downstream->upstreams.insert(source);
|
|
if (target->downstreams.count(downstream)) continue;
|
|
downstream->upstreams.erase(target);
|
|
}
|
|
// 3. Check topo index and update
|
|
const auto need_reorder_topo_index = [&]() {
|
|
for (const auto& up : source->upstreams)
|
|
if (up->topo_index >= source->topo_index) return true;
|
|
for (const auto& down : source->downstreams)
|
|
if (down->topo_index <= source->topo_index) return true;
|
|
for (const auto& up : target->upstreams)
|
|
if (up->topo_index >= target->topo_index) return true;
|
|
for (const auto& down : target->downstreams)
|
|
if (down->topo_index <= target->topo_index) return true;
|
|
return false;
|
|
};
|
|
if (need_reorder_topo_index()) ReorderIndexOfSubgraphs();
|
|
VLOG(6) << "After fallback subgraph fusion: "
|
|
<< "\n source: " << source->DebugStr()
|
|
<< "\n target: " << target->DebugStr();
|
|
}
|
|
|
|
void SubgraphDetector::InitInplaceOpsOrder(
|
|
const std::vector<Operation*>& inplace_ops) {
|
|
std::unordered_map<Value, Value> inplace_map;
|
|
const auto& get_inplace_root_value = [&inplace_map](const Value& value) {
|
|
Value root = value;
|
|
std::unordered_set<Value> visited;
|
|
while (inplace_map.count(root)) {
|
|
if (visited.count(root)) break;
|
|
visited.insert(root);
|
|
root = inplace_map.at(root);
|
|
}
|
|
return root;
|
|
};
|
|
std::vector<std::set<Value>> inplace_values_sets;
|
|
for (const auto& op : inplace_ops) {
|
|
auto output_input_values = GetInplaceValues(op);
|
|
std::set<Value> inplace_input_values;
|
|
for (const auto& output_input_value : output_input_values) {
|
|
inplace_input_values.insert(
|
|
get_inplace_root_value(output_input_value.second));
|
|
inplace_map[output_input_value.first] = output_input_value.second;
|
|
}
|
|
inplace_values_sets.push_back(inplace_input_values);
|
|
}
|
|
std::set<Value> shared_inplace_values_set;
|
|
for (size_t i = 0; i < inplace_values_sets.size(); ++i) {
|
|
for (size_t j = i + 1; j < inplace_values_sets.size(); ++j) {
|
|
std::set_intersection(inplace_values_sets[i].begin(),
|
|
inplace_values_sets[i].end(),
|
|
inplace_values_sets[j].begin(),
|
|
inplace_values_sets[j].end(),
|
|
std::inserter(shared_inplace_values_set,
|
|
shared_inplace_values_set.begin()));
|
|
}
|
|
}
|
|
std::vector<std::vector<Operation*>> inplace_ops_order;
|
|
std::vector<Value> shared_inplace_values;
|
|
for (const auto& shared_value : shared_inplace_values_set) {
|
|
inplace_ops_order.emplace_back();
|
|
shared_inplace_values.push_back(shared_value);
|
|
for (size_t i = 0; i < inplace_values_sets.size(); ++i) {
|
|
if (inplace_values_sets[i].count(shared_value)) {
|
|
inplace_ops_order.back().push_back(inplace_ops[i]);
|
|
}
|
|
}
|
|
}
|
|
// If a value is inplaced by multiple ops, the order of ops which use this
|
|
// value after different inplace op also needs to be considered together.
|
|
for (size_t i = 0; i < inplace_ops_order.size(); ++i) {
|
|
auto only_inplace_ops = inplace_ops_order[i];
|
|
auto inplace_root_value = shared_inplace_values[i];
|
|
std::unordered_set<Operation*> ordered_ops_set(only_inplace_ops.begin(),
|
|
only_inplace_ops.end());
|
|
for (const auto& inplace_op : only_inplace_ops) {
|
|
Value output_inplace_value;
|
|
auto output_input_values = GetInplaceValues(inplace_op);
|
|
for (const auto& [output_value, _unused] : output_input_values) {
|
|
if (get_inplace_root_value(output_value) == inplace_root_value) {
|
|
output_inplace_value = output_value;
|
|
break;
|
|
}
|
|
}
|
|
if (output_inplace_value.use_empty()) continue;
|
|
for (auto use_iter = output_inplace_value.use_begin();
|
|
use_iter != output_inplace_value.use_end();
|
|
++use_iter) {
|
|
auto user_op = use_iter.owner();
|
|
if (ordered_ops_set.count(user_op)) continue;
|
|
ordered_ops_set.insert(user_op);
|
|
}
|
|
}
|
|
// Sort by origin order in blocks
|
|
std::vector<Operation*> ordered_ops(ordered_ops_set.begin(),
|
|
ordered_ops_set.end());
|
|
std::sort(ordered_ops.begin(),
|
|
ordered_ops.end(),
|
|
[this](const auto& lhs, const auto& rhs) {
|
|
return this->op2index_.at(lhs) < this->op2index_.at(rhs);
|
|
});
|
|
this->inplace_ops_order_.push_back(ordered_ops);
|
|
}
|
|
}
|
|
|
|
SubgraphDetector::SubgraphDetector(Block* block,
|
|
const OpClassifier& classifier) {
|
|
// init sort_ops_ in reverse topo order and op2index_ in topo order
|
|
std::vector<Operation*> inplace_ops;
|
|
int index = 0;
|
|
for (auto& op : *block) {
|
|
sort_ops_.push_back(&op);
|
|
op2index_[&op] = index++;
|
|
if (IsSideEffectButNotInplaceOp(&op)) {
|
|
side_effect_ops_.push_back(&op);
|
|
}
|
|
if (op.HasTrait<paddle::dialect::InplaceTrait>()) {
|
|
inplace_ops.push_back(&op);
|
|
}
|
|
}
|
|
std::reverse(sort_ops_.begin(), sort_ops_.end());
|
|
InitInplaceOpsOrder(inplace_ops);
|
|
|
|
// construct subgraphs and upstream/downstream relation
|
|
std::vector<SubGraphPtr> subgraph_list;
|
|
for (const auto& op : sort_ops_) {
|
|
bool substitute = classifier(*op);
|
|
auto subgraph = std::make_shared<SubGraph>(op, op2index_[op], substitute);
|
|
op2subgraph_[op] = subgraph;
|
|
subgraph_index_set_.insert(op2index_[op]);
|
|
subgraph_list.push_back(subgraph);
|
|
}
|
|
for (const auto& op : sort_ops_) {
|
|
auto subgraph = op2subgraph_[op];
|
|
for (const auto& producer : GetProducerOps(op)) {
|
|
if (!op2subgraph_.count(producer)) continue;
|
|
subgraph->upstreams.insert(op2subgraph_[producer]);
|
|
op2subgraph_[producer]->downstreams.insert(subgraph);
|
|
}
|
|
for (const auto& consumer : GetConsumerOps(op, op2index_)) {
|
|
if (!op2subgraph_.count(consumer)) continue;
|
|
subgraph->downstreams.insert(op2subgraph_[consumer]);
|
|
op2subgraph_[consumer]->upstreams.insert(subgraph);
|
|
}
|
|
}
|
|
|
|
VLOG(6) << "Subgraphs before building groups: ";
|
|
for (const auto& subgraph : subgraph_list) {
|
|
VLOG(6) << subgraph->DebugStr();
|
|
}
|
|
auto circle_info = DetectCirclesInSubgraphs(subgraph_list);
|
|
if (circle_info) {
|
|
PADDLE_THROW(::common::errors::PreconditionNotMet(
|
|
"Before building groups: %s", circle_info.value()));
|
|
}
|
|
}
|
|
|
|
void SubgraphDetector::SubgraphFusion() {
|
|
// Two subgraphs can be merged only if they have no route except direct
|
|
// connection between them (brother subgraphs should have no any route),
|
|
// otherwise a circle will be formed after merging them.
|
|
VLOG(4) << "Merge subgraphs with direct relation";
|
|
for (const auto& op : sort_ops_) {
|
|
auto downstream = GetOpSubgraph(op);
|
|
if (!downstream->substitute) continue;
|
|
for (const auto& producer : GetProducerOpsReverseSort(op, op2index_)) {
|
|
auto upstream = GetOpSubgraph(producer);
|
|
if (upstream == downstream || !upstream->substitute) continue;
|
|
if (CanFuseUpstream2Downstream(upstream, downstream)) {
|
|
MergeSource2Target(upstream, downstream);
|
|
}
|
|
}
|
|
}
|
|
|
|
VLOG(4) << "Merge brother subgraphs with same upstream";
|
|
for (const auto& op : sort_ops_) {
|
|
auto subgraph = GetOpSubgraph(op);
|
|
if (!subgraph->substitute) continue;
|
|
for (auto producer : GetProducerOpsReverseSort(op, op2index_)) {
|
|
if (GetOpSubgraph(producer) == subgraph) continue;
|
|
for (auto consumer : GetConsumerOps(producer, op2index_)) {
|
|
auto brother = GetOpSubgraph(consumer);
|
|
if (brother == subgraph || !brother->substitute) continue;
|
|
if (!HasRoute(subgraph, brother) && !HasRoute(brother, subgraph)) {
|
|
MergeSource2Target(brother, subgraph);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
auto subgraph_list = GetSubgraphList();
|
|
VLOG(4) << "Merge non-related subgraphs (size=" << subgraph_list.size()
|
|
<< ")";
|
|
if (subgraph_list.size() > 2048 && !FLAGS_merge_all_horizontal_groups) return;
|
|
for (size_t i = 0; i < subgraph_list.size(); ++i) {
|
|
auto lhs = subgraph_list[i];
|
|
if (!lhs->substitute) continue;
|
|
for (size_t j = i + 1; j < subgraph_list.size();) {
|
|
auto rhs = subgraph_list[j];
|
|
if (lhs == rhs || !rhs->substitute || HasRoute(lhs, rhs) ||
|
|
HasRoute(rhs, lhs)) {
|
|
++j;
|
|
continue;
|
|
}
|
|
SubGraph lhs_back = *lhs;
|
|
SubGraph rhs_back = *rhs;
|
|
if (MergeSource2Target(rhs, lhs)) {
|
|
subgraph_list.erase(subgraph_list.begin() + j);
|
|
} else {
|
|
++j;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<GroupOpsVec> SubgraphDetector::BuildGroups() {
|
|
// 1. Get subgraph list in topo order
|
|
auto subgraph_list = GetSubgraphList();
|
|
std::reverse(subgraph_list.begin(), subgraph_list.end());
|
|
VLOG(6) << "Subgraphs after building groups: ";
|
|
for (const auto& subgraph : subgraph_list) {
|
|
VLOG(6) << subgraph->DebugStr();
|
|
}
|
|
auto circle_info = DetectCirclesInSubgraphs(subgraph_list);
|
|
if (circle_info) {
|
|
PADDLE_THROW(::common::errors::PreconditionNotMet(
|
|
"After building groups: %s", circle_info.value()));
|
|
}
|
|
|
|
// 2. Build group ops in subgraph which can be substituted
|
|
std::vector<GroupOpsVec> groups;
|
|
for (const auto& subgraph : subgraph_list) {
|
|
if (!subgraph->substitute) {
|
|
continue;
|
|
}
|
|
// sort group ops by natural increasing index.
|
|
std::vector<Operation*> group_ops(subgraph->ops.begin(),
|
|
subgraph->ops.end());
|
|
std::sort(
|
|
group_ops.begin(), group_ops.end(), [this](Operation* a, Operation* b) {
|
|
return this->op2index_.at(a) < this->op2index_.at(b);
|
|
});
|
|
groups.push_back(group_ops);
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
std::vector<GroupOpsVec> DetectSubGraphs(Block* block,
|
|
const OpClassifier& classifier) {
|
|
auto subgraph_detector = SubgraphDetector(block, classifier);
|
|
subgraph_detector.SubgraphFusion();
|
|
return subgraph_detector.BuildGroups();
|
|
}
|
|
|
|
std::vector<Value> AnalysisOutputs(const GroupOpsVec& group_ops,
|
|
bool at_least_one_output) {
|
|
// Get output by ud chain
|
|
std::unordered_set<Operation*> op_set(group_ops.begin(), group_ops.end());
|
|
|
|
std::vector<Value> outputs;
|
|
for (auto* op : group_ops) {
|
|
for (size_t i = 0; i < op->num_results(); ++i) {
|
|
auto result = op->result(i);
|
|
|
|
for (auto use_iter = result.use_begin(); use_iter != result.use_end();
|
|
++use_iter) {
|
|
if (!op_set.count(use_iter->owner())) {
|
|
outputs.push_back(result);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// NOTE: If all value are not used outside, we mark last op's results
|
|
// as outputs. But keep in mind that is risky.
|
|
if (at_least_one_output && outputs.size() == 0) {
|
|
for (size_t i = 0; i < group_ops.back()->num_results(); ++i) {
|
|
outputs.push_back(group_ops.back()->result(i));
|
|
}
|
|
}
|
|
|
|
return outputs;
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct IncrementalOrder {
|
|
bool operator()(const Operation* lhs, const Operation* rhs) const {
|
|
PADDLE_ENFORCE_EQ(lhs->GetParent() == rhs->GetParent(),
|
|
true,
|
|
common::errors::PreconditionNotMet(
|
|
"lhs and rhs should have same parent block."));
|
|
auto lhs_iter = lhs->operator Block::ConstIterator();
|
|
auto rhs_iter = rhs->operator Block::ConstIterator();
|
|
auto end_iter = lhs->GetParent()->end();
|
|
while (lhs_iter != end_iter) {
|
|
lhs_iter++;
|
|
if (lhs_iter == rhs_iter) return true;
|
|
if (lhs_iter == end_iter) return false;
|
|
}
|
|
PADDLE_ENFORCE_EQ(
|
|
false,
|
|
true,
|
|
common::errors::InvalidArgument("rhs is not reachable from lhs."));
|
|
return false;
|
|
}
|
|
};
|
|
|
|
std::unordered_set<Operation*> GetUpstreamOpsAfterPosition(
|
|
const Operation* position_op,
|
|
const Block* block,
|
|
Operation* op,
|
|
std::unordered_set<Operation*>* visited_ops) {
|
|
std::unordered_set<Operation*> ops;
|
|
const auto& IsInBlock = [](const Operation* src_op, const Block* block) {
|
|
for (auto& item : *block) {
|
|
if (src_op->id() == item.id()) return true;
|
|
}
|
|
return false;
|
|
};
|
|
std::vector<Value> op_inputs = GetUsedExternalValue(*op);
|
|
for (auto value : op_inputs) {
|
|
if (!value || !value.defining_op()) continue;
|
|
Operation* defining_op = value.defining_op();
|
|
if (visited_ops->count(defining_op)) continue;
|
|
visited_ops->insert(defining_op);
|
|
if (!IsInBlock(defining_op, block)) continue;
|
|
if (IncrementalOrder()(defining_op, position_op)) continue;
|
|
|
|
ops.insert(defining_op);
|
|
auto recursive_ops = GetUpstreamOpsAfterPosition(
|
|
position_op, block, defining_op, visited_ops);
|
|
ops.insert(recursive_ops.begin(), recursive_ops.end());
|
|
}
|
|
return ops;
|
|
}
|
|
} // namespace
|
|
|
|
void MoveUpstreamOpBeforeGroup(const GroupOpsVec& group_ops,
|
|
Block* block,
|
|
Operation* insert_point_op) {
|
|
const auto moved_ops = [&]() {
|
|
std::set<Operation*, IncrementalOrder> ops_set;
|
|
std::unordered_set<Operation*> visited_ops;
|
|
for (auto& op : group_ops) {
|
|
auto upstream_ops =
|
|
GetUpstreamOpsAfterPosition(insert_point_op, block, op, &visited_ops);
|
|
ops_set.insert(upstream_ops.begin(), upstream_ops.end());
|
|
}
|
|
return ops_set;
|
|
}();
|
|
|
|
for (auto& op : moved_ops) {
|
|
if (op == insert_point_op) continue;
|
|
VLOG(4) << "Move " << op->id() << " " << op->name() << " before "
|
|
<< insert_point_op->id() << " " << insert_point_op->name();
|
|
op->MoveTo(block, insert_point_op->operator Block::Iterator());
|
|
}
|
|
}
|
|
|
|
Operation* FindInsertPoint(const GroupOpsVec& group_ops,
|
|
const std::vector<Value>& outputs) {
|
|
// Regard last op as insert position if there are no downstream ops between in
|
|
// group_ops.
|
|
Operation* first_op = group_ops.front();
|
|
Operation* insert_point_op = group_ops.back();
|
|
auto order_info = [&]() -> std::unordered_map<const Operation*, int64_t> {
|
|
std::unordered_map<const Operation*, int64_t> map;
|
|
// initialize the position index with block size by default.
|
|
auto block = insert_point_op->GetParent();
|
|
int64_t order = 0;
|
|
for (auto& op : *block) {
|
|
map[&op] = order++;
|
|
}
|
|
return map;
|
|
}();
|
|
|
|
for (auto* op : group_ops) {
|
|
if (order_info.at(op) > order_info.at(insert_point_op)) {
|
|
insert_point_op = op;
|
|
}
|
|
if (order_info.at(op) < order_info.at(first_op)) {
|
|
first_op = op;
|
|
}
|
|
}
|
|
|
|
auto begin = first_op->operator Block::ConstIterator();
|
|
auto end = ++(insert_point_op->operator Block::ConstIterator());
|
|
const std::unordered_set<Value> outputs_set(outputs.begin(), outputs.end());
|
|
const std::unordered_set<const Operation*> group_ops_set(group_ops.begin(),
|
|
group_ops.end());
|
|
|
|
const auto& IsDownstreamOp = [&](const Operation* op) -> bool {
|
|
if (group_ops_set.find(op) != group_ops_set.end()) return false;
|
|
for (auto& value : GetUsedExternalValue(*op)) {
|
|
if (outputs_set.find(value) != outputs_set.end()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
// Find first downstream op as final insert position.
|
|
for (; begin != end; ++begin) {
|
|
if (IsDownstreamOp(begin)) {
|
|
insert_point_op = begin;
|
|
break;
|
|
}
|
|
}
|
|
return insert_point_op;
|
|
}
|
|
|
|
void ReplaceWithGroupOp(Block* block,
|
|
const GroupOpsVec& group_ops,
|
|
bool at_least_one_output) { // NOLINT
|
|
IrContext* ctx = IrContext::Instance();
|
|
#ifdef PADDLE_WITH_CINN
|
|
ctx->GetOrRegisterDialect<cinn::dialect::OperatorDialect>();
|
|
#endif
|
|
#ifdef PADDLE_WITH_DNNL
|
|
ctx->GetOrRegisterDialect<paddle::dialect::OneDNNOperatorDialect>();
|
|
#endif
|
|
Builder builder = Builder(ctx, block);
|
|
const std::vector<Value> outputs =
|
|
AnalysisOutputs(group_ops, at_least_one_output);
|
|
|
|
// step 1: Analysis and insert group op before insert_point.
|
|
auto* insert_point = FindInsertPoint(group_ops, outputs);
|
|
MoveUpstreamOpBeforeGroup(group_ops, block, insert_point);
|
|
builder.set_insertion_point(insert_point);
|
|
VLOG(6) << "Insert GroupOp after " << insert_point->name();
|
|
|
|
// step 2: Replace the old op with GroupOp.
|
|
#ifdef PADDLE_WITH_CINN
|
|
|
|
auto new_group_op = [&]() -> cinn::dialect::GroupOp {
|
|
std::vector<Type> output_types;
|
|
for (auto& value : outputs) output_types.emplace_back(value.type());
|
|
|
|
auto group_op = builder.Build<cinn::dialect::GroupOp>(output_types);
|
|
for (auto op : group_ops) {
|
|
op->MoveTo(group_op.block(), group_op.block()->end());
|
|
}
|
|
return group_op;
|
|
}();
|
|
#else
|
|
auto new_group_op = [&]() -> GroupOp {
|
|
std::vector<Type> output_types;
|
|
for (auto& value : outputs) output_types.emplace_back(value.type());
|
|
|
|
auto group_op = builder.Build<GroupOp>(output_types);
|
|
for (auto op : group_ops) {
|
|
op->MoveTo(group_op.block(), group_op.block()->end());
|
|
}
|
|
return group_op;
|
|
}();
|
|
#endif
|
|
|
|
// step 3: Replace outputs of inner ops
|
|
const std::vector<Value> group_outs = new_group_op->results();
|
|
std::unordered_set<Operation*> inner_ops(group_ops.begin(), group_ops.end());
|
|
for (size_t i = 0; i < outputs.size(); ++i) {
|
|
outputs[i].ReplaceUsesWithIf(group_outs[i], [&inner_ops](OpOperand op) {
|
|
return !inner_ops.count(op.owner());
|
|
});
|
|
}
|
|
|
|
// step 4: Insert YieldOp for outputs
|
|
builder.SetInsertionPointToBlockEnd(new_group_op.block());
|
|
builder.Build<YieldOp>(outputs);
|
|
}
|
|
|
|
} // namespace pir
|