| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| */ |
| |
| /*! |
| * \file src/relax/transform/fuse_ops.cc |
| * \brief This file contains a pass which groups bindings in a dataflow block of Relax |
| * functions and generate a new grouped Relax function for each group, according to the fusion |
| * algorithm described below. By grouping bindings into new Relax functions, we substitute the |
| * bindings in the function being manipulated into function calls to the new grouped function. |
| * |
| * A follow-up pass named "FuseTIR" will generate a TIR PrimFunc for each grouped function. |
| */ |
| |
| #include <tvm/ffi/cast.h> |
| #include <tvm/ffi/reflection/registry.h> |
| #include <tvm/relax/analysis.h> |
| #include <tvm/relax/dataflow_matcher.h> |
| #include <tvm/relax/dataflow_pattern.h> |
| #include <tvm/relax/expr_functor.h> |
| #include <tvm/relax/transform.h> |
| #include <tvm/relax/type.h> |
| #include <tvm/relax/utils.h> |
| #include <tvm/runtime/logging.h> |
| #include <tvm/tirx/analysis.h> |
| #include <tvm/tirx/expr_functor.h> |
| #include <tvm/tirx/function.h> |
| |
| #include <optional> |
| |
| #include "../../support/arena.h" |
| #include "../analysis/graph_partitioner.h" |
| #include "tvm/relax/expr.h" |
| #include "utils.h" |
| |
| namespace tvm { |
| namespace relax { |
| |
| struct ExprIdentityLess { |
| bool operator()(const Expr& lhs, const Expr& rhs) const { return lhs.get() < rhs.get(); } |
| }; |
| |
| TVM_FFI_STATIC_INIT_BLOCK() { |
| transform::FusionPatternNode::RegisterReflection(); |
| transform::PatternCheckContextNode::RegisterReflection(); |
| } |
| |
| /* |
| Note on Fusing algorithm: |
| |
| The main challenge of general fusor is to handle possible diamond shape branches, |
| in the following graph, conv2d can be fused to elemwise add. |
| |
| conv2d |
| / | \ |
| / | \ |
| op op op |
| \ | / |
| \ | / |
| elemwise add |
| | |
| |
| However, at the point of conv2d we do not necessarily know that all the future paths |
| will merge at the elemwise add. The fusion algorithm applies post-dominator analysis. |
| |
| The immediate post-dominator of a node defined by the closest node where all the future path goes |
| into. In the above case, the elemwise add is the post-dominator of conv2d. The general algorithm |
| is as follows: |
| |
| - Construct a DAG of dataflow graph for dominator analysis |
| - Construct a post-dominator tree which gives immediate post dominator of each node. |
| - Run fusion algorithm with the given post-dominator information. |
| |
| Note that, because we run analysis on a DAG, we use a single pass post-dominator |
| tree construction algorithm via LCA, which is simpler than the full version that handles cycles. |
| |
| The fusion algorithm traverses from each node and checks if it can be fused to its |
| immediate post dominator. It has to check the following things: |
| |
| - CheckPath: check all the path between a node and its immediate post-dominator |
| satisfies the fuse condition. |
| - Note that these intermediate node can already be fused with another nodes, the algorithm |
| will still run correctly. |
| - CommitFuse: mark all the nodes between source and post-dominator as the same group. |
| - We use an Union-Find data structure to manage the groups. |
| */ |
| |
| using support::LinkNode; |
| |
| constexpr uint32_t kMaxFusedOps = 256; |
| |
| TVM_REGISTER_PASS_CONFIG_OPTION("relax.FuseOps.max_depth", int64_t); |
| |
| class GraphCreator : public ExprVisitor { |
| public: |
| /*! |
| * \brief Create a IndexedForwardGraph according to the input module. The graph will be used for |
| * graph partition and operator fusion. |
| * \param mod The module which the creation accords to |
| * \param arena The allocator of all the internal node objects |
| * \return The created IndexedForwardGraph |
| */ |
| static IndexedForwardGraph Create(IRModule mod, support::Arena* arena) { |
| GraphCreator creator(mod, arena); |
| for (const auto& it : mod->functions) { |
| // Only visit Relax functions with neither attr::kPrimitive nor |
| // attr::kCodegen. Relax functions with `attr::kPrimitive` are |
| // previously fused functions, potentially from a previous use |
| // of `FuseOps` or `FuseOpsByPattern`. Relax functions with |
| // `attr::kCodegen` are previously fused functions from |
| // `FuseOpsByPattern`, when the `annotate_codegen` option is |
| // true. |
| const auto* func = it.second.as<FunctionNode>(); |
| if (func == nullptr || func->HasNonzeroAttr(attr::kPrimitive) || |
| func->GetAttr<ffi::String>(attr::kCodegen).has_value()) { |
| continue; |
| } |
| creator(ffi::GetRef<Function>(func)); |
| } |
| |
| // The algorithm of the graph creator ensures that each created node will be added to the |
| // post-dfs order and will be set its op pattern. Thus we check whether all these containers |
| // have the same size. |
| size_t n_nodes = creator.graph_.node_map.size(); |
| TVM_FFI_ICHECK_EQ(n_nodes, creator.graph_.post_dfs_order.size()); |
| TVM_FFI_ICHECK_EQ(n_nodes, creator.initialized_nodes_.size()); |
| |
| return creator.graph_; |
| } |
| |
| private: |
| explicit GraphCreator(IRModule mod, support::Arena* arena) |
| : mod_(std::move(mod)), arena_(arena) {} |
| |
| void VisitExpr_(const FunctionNode* func) final { |
| for (const Var& param : func->params) { |
| IndexedForwardGraph::Node* param_node = CreateNode(param.get()); |
| // The parameter is passed in from the outside, and thus it's marked as an external reference, |
| // and it's pattern is `kOpaque`. |
| MarkAsExternRef(param_node); |
| SetNodePattern(param_node, OpPatternKind::kOpaque); |
| AddToPostDFSOrder(param_node, param.get()); |
| } |
| if (auto opt_num_input = func->GetAttr<int64_t>(attr::kNumInput)) { |
| for (int i = static_cast<int>(opt_num_input.value()); |
| i < static_cast<int>(func->params.size()); ++i) { |
| input_params_.insert(func->params[i].get()); |
| } |
| } |
| ExprVisitor::VisitExpr_(func); |
| } |
| |
| void VisitBinding_(const MatchCastNode* binding) final { |
| IndexedForwardGraph::Node* node = CreateNode(binding->var.get()); |
| SetNodePattern(node, OpPatternKind::kOpaque); |
| AddToPostDFSOrder(node, binding->var.get()); |
| } |
| |
| void VisitBinding_(const VarBindingNode* binding) final { |
| IndexedForwardGraph::Node* node = CreateNode(binding->var.get()); |
| |
| // If the variable is not a dataflow variable, it must be the output variable of this dataflow |
| // block |
| if (!binding->var->IsInstance<DataflowVarNode>()) { |
| this->MarkAsExternRef(node); |
| } |
| if (const auto* call = binding->value.as<CallNode>()) { |
| // Case 1. The expression is a CallNode |
| VisitCall(call, node); |
| } else if (const auto* tuple_get_item = binding->value.as<TupleGetItemNode>()) { |
| // Case 2. The expression is a TupleGetItemNode |
| VisitTupleGetItem(tuple_get_item, node); |
| } else { |
| VisitUnsupportedNode(binding->value, node); |
| // Case 3. The type of the expression is not fusion-supported. |
| // In this case, we skip adding edges, adding an empty node into graph. |
| } |
| AddToPostDFSOrder(node, binding->var.get()); |
| } |
| |
| /********** Non-Leaf Expression Nodes **********/ |
| |
| void VisitCall(const CallNode* call, IndexedForwardGraph::Node* binding_var_node) { |
| TVM_FFI_ICHECK_NOTNULL(binding_var_node); |
| |
| static const Op& call_tir_op_ = Op::Get("relax.call_tir"); |
| static const Op& call_tir_inplace_op_ = Op::Get("relax.call_tir_inplace"); |
| |
| OpPatternKind pattern = OpPatternKind::kOpaque; |
| ffi::Array<Expr> args = call->args; |
| |
| // - If the op being called is a TIR PrimFunc, we get the function op pattern directly from the |
| // function attribute and visit the arguments one by one. |
| // - Otherwise, the pattern of the current binding variable node is set to `kOpaque`, and we |
| // recurse into the call expression. |
| const auto* op = call->op.as<OpNode>(); |
| if (op == call_tir_op_.get() || op == call_tir_inplace_op_.get()) { |
| const GlobalVar& global_var = call->args[0].as_or_throw<GlobalVar>(); |
| tirx::PrimFunc func = mod_->Lookup(global_var).as_or_throw<tirx::PrimFunc>(); |
| |
| // Override args for call_tir |
| args = call->args[1].as_or_throw<Tuple>()->fields; |
| |
| ffi::Optional<int64_t> opt_pattern = func->GetAttr<int64_t>("op_pattern"); |
| if (opt_pattern.has_value()) { |
| pattern = static_cast<OpPatternKind>(opt_pattern.value()); |
| } else { |
| pattern = OpPatternKind::kOpaque; |
| } |
| } |
| // The pattern of the current binding variable node is set to the pattern of this operator. |
| SetNodePattern(binding_var_node, pattern); |
| // Visit all call args |
| for (const Expr& arg : args) { |
| TVM_FFI_ICHECK(IsLeafOrTuple(arg)) |
| << "FuseOps expects all relax::Call nodes to have non-nested arguments, " |
| << "but " << ffi::GetRef<Expr>(call) << " has argument " << arg |
| << ", which is neither a leaf node nor a relax::Tuple"; |
| VisitLeaf(arg, binding_var_node, pattern); |
| } |
| } |
| |
| void VisitTupleGetItem(const TupleGetItemNode* tuple_item, |
| IndexedForwardGraph::Node* binding_var_node) { |
| TVM_FFI_ICHECK_NOTNULL(binding_var_node); |
| |
| auto pattern = OpPatternKind::kInjective; |
| if (input_params_.count(tuple_item->tuple.as<VarNode>())) { |
| // TupleGetItem for fetching the parameter from the packed param tuple is treated as opaque |
| // and won't be fused. This prevents the usage of packed param tuple changes the order of the |
| // fusion result as the function usually begins with fetching the parameters. |
| pattern = OpPatternKind::kOpaque; |
| } |
| SetNodePattern(binding_var_node, pattern); |
| VisitLeaf(tuple_item->tuple, binding_var_node, pattern); |
| } |
| |
| void VisitUnsupportedNode(const Expr& expr, IndexedForwardGraph::Node* binding_var_node) { |
| TVM_FFI_ICHECK_NOTNULL(binding_var_node); |
| SetNodePattern(binding_var_node, OpPatternKind::kOpaque); |
| |
| auto visit_leaves = [this, &binding_var_node](const Expr& e) { |
| if (e->IsInstance<VarNode>() || e->IsInstance<ConstantNode>()) { |
| VisitLeaf(e, binding_var_node, OpPatternKind::kOpaque); |
| } |
| }; |
| PostOrderVisit(expr, visit_leaves); |
| } |
| |
| /********** Leaf Expression Nodes **********/ |
| |
| void VisitLeaf(const Expr& leaf_expr, IndexedForwardGraph::Node* binding_var_node, |
| const OpPatternKind& pattern) { |
| TVM_FFI_ICHECK_NOTNULL(binding_var_node); |
| |
| // Recursive visit if it's Tuple |
| if (const auto* tuple = leaf_expr.as<TupleNode>()) { |
| for (const Expr& expr : tuple->fields) { |
| VisitLeaf(expr, binding_var_node, pattern); |
| } |
| return; |
| } |
| |
| if (!leaf_expr.as<ShapeExprNode>() && !leaf_expr.as<VarNode>() && |
| !leaf_expr.as<ConstantNode>() && !leaf_expr.as<PrimExpr>() && |
| !leaf_expr.as<StringImmNode>() && !leaf_expr.as<DataTypeImmNode>()) { |
| // Skip GlobalVar, ExternFunc, OpNode. |
| return; |
| } |
| |
| auto it = graph_.node_map.find(leaf_expr.get()); |
| IndexedForwardGraph::Node* leaf_node = nullptr; |
| if (it != graph_.node_map.end()) { |
| leaf_node = it->second; |
| } else { |
| leaf_node = CreateNode(leaf_expr.get()); |
| // Since we never fuse constants, the pattern of the constant is set to `kOpaque`. |
| SetNodePattern(leaf_node, OpPatternKind::kOpaque); |
| AddToPostDFSOrder(leaf_node, leaf_expr.get()); |
| } |
| AddEdge(leaf_node, binding_var_node, pattern); |
| } |
| |
| /********** Helper Functions **********/ |
| |
| /*! |
| * \brief Create a graph node corresponding to the input key |
| * \param key The object which is used to create the graph node |
| * \return The created graph node |
| * \note The node corresponding to each key is supposed to be created for only once |
| */ |
| IndexedForwardGraph::Node* CreateNode(const ffi::Object* key) { |
| TVM_FFI_ICHECK(graph_.node_map.find(key) == graph_.node_map.end()) |
| << "The object " << ffi::GetRef<ffi::ObjectRef>(key) |
| << " appears at multiple definition sites."; |
| auto* node = arena_->make<IndexedForwardGraph::Node>(); |
| graph_.node_map[key] = node; |
| return node; |
| } |
| |
| /*! |
| * \brief Append the input node to the post-dfs order of the graph |
| * \param node The node to be appended |
| * \param key The key corresponding to the node |
| * \note Each node is supposed to be appended to the post-dfs order for only once |
| */ |
| void AddToPostDFSOrder(IndexedForwardGraph::Node* node, const ffi::Object* key) { |
| auto it = graph_.node_map.find(key); |
| TVM_FFI_ICHECK(it != graph_.node_map.end() && it->second == node) |
| << "Cannot add node " << ffi::GetRef<ffi::ObjectRef>(key) << " to the post-DFS order, " |
| << "because the node for this object has not yet been created."; |
| |
| // We only set the reference of the node when adding it to the post-dfs order. Thus, if the |
| // reference of a node is already set, it must have been appended to the post-dfs order. |
| TVM_FFI_ICHECK(node->ref == nullptr) |
| << "Cannot add node " << ffi::GetRef<ffi::ObjectRef>(key) << " to the post-DFS order, " |
| << "because it has already been added."; |
| |
| node->ref = key; |
| node->index = graph_.post_dfs_order.size(); |
| graph_.post_dfs_order.push_back(node); |
| } |
| |
| /*! |
| * \brief Add an edge from the input start to the input end in the graph, with specific pattern |
| * \param start The start of the edge |
| * \param end The end of the edge |
| * \param pattern The pattern of this edge |
| */ |
| void AddEdge(IndexedForwardGraph::Node* start, IndexedForwardGraph::Node* end, |
| OpPatternKind pattern) { |
| auto* link = arena_->make<LinkNode<IndexedForwardGraph::Edge>>(); |
| link->value.node = end; |
| link->value.pattern = pattern; |
| start->outputs.Push(link); |
| } |
| |
| /*! |
| * \brief Mark a given node as "external reference", which means the node cannot be fused as an |
| * intermediate node |
| * \param node The graph node to be marked |
| */ |
| void MarkAsExternRef(IndexedForwardGraph::Node* node) { node->extern_ref = true; } |
| |
| /*! |
| * \brief Set the pattern of the input node |
| * \param node The graph node to be set |
| * \param pattern The pattern of the node |
| */ |
| void SetNodePattern(IndexedForwardGraph::Node* node, OpPatternKind pattern) { |
| TVM_FFI_ICHECK(initialized_nodes_.find(node) == initialized_nodes_.end()) |
| << "The input node " << ffi::GetRef<ffi::ObjectRef>(node->ref) |
| << " cannot have have its OpPatternKind set more than once."; |
| initialized_nodes_.insert(node); |
| node->pattern = pattern; |
| } |
| |
| private: |
| /*! \brief The IRModule from which the indexed forward graph is created */ |
| IRModule mod_; |
| /*! \brief The allocator of all the internal node objects */ |
| support::Arena* arena_; |
| /*! \brief The created indexed forward graph */ |
| IndexedForwardGraph graph_; |
| /*! \brief The graph nodes whose patterns are set */ |
| std::unordered_set<IndexedForwardGraph::Node*> initialized_nodes_; |
| /*! \brief The model params in the function input */ |
| std::unordered_set<const VarNode*> input_params_; |
| }; |
| |
| /*! |
| * \brief The ExprMutator used to create a new grouped function |
| * \details The workflow of this ExprMutator is: |
| * - The bindings in the function will be added by OperatorFusor via `AppendBinding(...)`. |
| * - When adding a new binding through `AppendBinding(...)`, we check whether the variables and |
| * constants used by the binding are defined by some previous added binding. And for the undefined |
| * variables and constants, we add them to the argument list and created new variables as the |
| * corresponding parameters. |
| * - When `CreateFunction()` is called, we go through each binding and update the binding with the |
| * new parameters. After that we wrap all bindings with a DataflowBlock and a Function. |
| */ |
| class FunctionCreator : public ExprMutator { |
| public: |
| explicit FunctionCreator(bool lift_constant, ffi::Map<Var, Expr> outer_bindings) |
| : outer_bindings_(std::move(outer_bindings)), lift_constant_(lift_constant) {} |
| /*! |
| * \brief Append a new binding to this function and possibly create new parameters for the |
| * function accordingly |
| * \param binding The binding to be appended |
| * \note Allowed bindings are: |
| * - VarBinding with value being a call node calling `relax.call_tir` or |
| * `relax.call_tir_inplace`. |
| * - VarBinding with value being a tuple-get-item node. |
| * // TODO(tvm-team): handle match shape |
| */ |
| void AppendBinding(const Binding& binding) { |
| TVM_FFI_ICHECK(!function_.has_value()) |
| << "The `function_` is supposed to be uncreated when adding bindings"; |
| |
| if (const auto* var_binding = binding.as<VarBindingNode>()) { |
| if (const auto* call = var_binding->value.as<CallNode>()) { |
| if (call->op.same_as(Op::Get("relax.call_tir")) || |
| call->op.same_as(Op::Get("relax.call_tir_inplace"))) { |
| // Update the name of the function. |
| name_hint_ = name_hint_ + "_" + call->args[0].as_or_throw<GlobalVar>()->name_hint; |
| |
| const Tuple& args = call->args[1].as_or_throw<Tuple>(); |
| for (const Expr& arg : args->fields) { |
| CheckDefAndUpdateParam(arg); |
| TVM_FFI_ICHECK(GetTypeAs<TupleTypeNode>(arg) == nullptr); |
| } |
| // TODO(tvm-team): handle shape expr |
| } else { |
| if (call->op->IsInstance<OpNode>()) { |
| name_hint_ = name_hint_ + "_" + call->op.as_or_throw<Op>()->name; |
| } else if (call->op->IsInstance<GlobalVarNode>()) { |
| std::string gvar_name = call->op.as_or_throw<GlobalVar>()->name_hint; |
| if (auto pos = gvar_name.find("fused_"); pos == 0) { |
| name_hint_ = name_hint_ + "_" + gvar_name.substr(std::string("fused_").size()); |
| } else { |
| name_hint_ = name_hint_ + "_" + gvar_name; |
| } |
| } |
| |
| for (const Expr& arg : call->args) { |
| if (auto tuple = arg.as<TupleNode>()) { |
| for (const Expr& tup_arg : tuple->fields) { |
| CheckDefAndUpdateParam(tup_arg); |
| TVM_FFI_ICHECK(GetTypeAs<TupleTypeNode>(tup_arg) == nullptr); |
| } |
| } else { |
| CheckDefAndUpdateParam(arg); |
| } |
| if (GetTypeAs<TupleTypeNode>(arg) != nullptr) { |
| // The argument is fully referenced. Thus we remove it from the mapping. |
| partially_used_tuple_params_.erase(arg.get()); |
| } |
| } |
| } |
| } else if (var_binding->value.as<TupleGetItemNode>()) { |
| const auto* tuple_item = var_binding->value.as<TupleGetItemNode>(); |
| CheckDefAndUpdateParam(tuple_item->tuple); |
| |
| if (partially_used_tuple_params_.find(tuple_item->tuple.get()) != |
| partially_used_tuple_params_.end()) { |
| // Appending get-item index to the mapping. |
| partially_used_tuple_params_[tuple_item->tuple.get()].push_back(tuple_item->index); |
| } |
| } |
| |
| // Mark the binding variable as defined. |
| defined_vars_.insert(var_binding->var.get()); |
| // Set var as output true if the binding is not a dataflow variable |
| if (!var_binding->var->IsInstance<DataflowVarNode>()) { |
| AppendOutput(var_binding->var); |
| } |
| } else { |
| // TODO(tvm-team): handle match_cast |
| } |
| bindings_.push_back(binding); |
| } |
| |
| /*! \brief Set a var defined in the group as output. */ |
| void AppendOutput(const Var& var) { |
| TVM_FFI_ICHECK(defined_vars_.count(var.get())); |
| if (GetOutputIndex(var)) return; |
| output_vars_.push_back(var.get()); |
| } |
| |
| /*! \brief Variables returned from the grouped function, in return-value order. */ |
| const std::vector<const VarNode*>& output_vars() const { return output_vars_; } |
| |
| /*! |
| * \brief Create the grouped function according to the collected bindings and parameters |
| * \param composite_name The name to identify the pattern this function is created from, if any. |
| * It will become the value of the kComposite attribute of the created function. |
| * \note The created function won't be returned immediately. It's stored in the `function_` field. |
| */ |
| void CreateFunction(ffi::Map<ffi::String, Any> group_attrs) { |
| // Step 1. Start constructing a new dataflow block. |
| builder_->BeginDataflowBlock(); |
| |
| // Step 2. Handing partially used tuple parameters: replacing entire tuple |
| // parameters with the parameters of its fields that are accessed in the |
| // function. |
| std::unordered_map<const ExprNode*, std::unordered_map<int, Var>> tuple_get_item_remap; |
| for (auto& [tuple_arg, item_indices] : partially_used_tuple_params_) { |
| TVM_FFI_ICHECK(!item_indices.empty()); |
| int param_idx = tuple_param_idx_[tuple_arg]; |
| Var param = params_[param_idx]; |
| ffi::String param_name = params_[param_idx]->name; |
| TupleType param_ty = tuple_arg->ty.as_or_throw<TupleType>(); |
| |
| ffi::Array<Expr> item_args; |
| ffi::Array<Var> item_params; |
| item_args.reserve(item_indices.size()); |
| item_params.reserve(item_indices.size()); |
| for (int item_idx : item_indices) { |
| Var item_param(param_name + "_" + std::to_string(item_idx), param_ty->fields[item_idx]); |
| item_args.push_back(TupleGetItem(ffi::GetRef<Expr>(tuple_arg), item_idx)); |
| item_params.push_back(item_param); |
| tuple_get_item_remap[tuple_arg][item_idx] = item_param; |
| } |
| arguments_.erase(arguments_.begin() + param_idx); |
| arguments_.insert(arguments_.begin() + param_idx, item_args.begin(), item_args.end()); |
| params_.erase(params_.begin() + param_idx); |
| params_.insert(params_.begin() + param_idx, item_params.begin(), item_params.end()); |
| } |
| |
| // Step 3. Visit each binding and collect outputs one by one. |
| ffi::Array<Expr> outputs(output_vars_.size(), Expr()); |
| for (const Binding& binding : bindings_) { |
| // Special handing for TupleGetItem. |
| if (const auto* var_binding = binding.as<VarBindingNode>()) { |
| if (const auto* tuple_get_item = var_binding->value.as<TupleGetItemNode>()) { |
| auto it = tuple_get_item_remap.find(tuple_get_item->tuple.get()); |
| if (it != tuple_get_item_remap.end()) { |
| TVM_FFI_ICHECK(it->second.find(tuple_get_item->index) != it->second.end()); |
| var_remap_[var_binding->var] = it->second[tuple_get_item->index]; |
| if (auto output_idx = GetOutputIndex(binding->var)) { |
| outputs.Set(*output_idx, it->second[tuple_get_item->index]); |
| } |
| continue; |
| } |
| } |
| } |
| |
| if (auto output_idx = GetOutputIndex(binding->var)) { |
| // Case 1. It is an output binding |
| // We only allow VarBinding as output. |
| const auto* var_binding = binding.as<VarBindingNode>(); |
| TVM_FFI_ICHECK_NOTNULL(var_binding); |
| Var output_var = builder_->EmitOutput(VisitExpr(var_binding->value)); |
| var_remap_[var_binding->var] = output_var; |
| outputs.Set(*output_idx, output_var); |
| } else { |
| // Case 2. It is an internal binding, add it to the binding list. |
| VisitBinding(binding); |
| } |
| } |
| |
| // Step 4. Finish constructing the new block. |
| BindingBlock new_block = builder_->EndBlock(); |
| if (outputs.empty()) { |
| // If the result is not used outside |
| LOG(WARNING) << "There are dead codes in the current IRModule, please run the " |
| "DeadCodeElimination Pass before FuseOps"; |
| function_ = std::nullopt; |
| } else { |
| Expr body = outputs.size() == 1 ? outputs[0] : Tuple(outputs); |
| body = builder_->Normalize(body); |
| body = builder_->Normalize(SeqExpr({new_block}, body)); |
| group_attrs.Set(tvm::relax::attr::kPrimitive, true); |
| Function function = Function(/*params=*/params_, // |
| /*body=*/body, // |
| /*ret_ty=*/std::nullopt, // |
| /*is_pure=*/true, // |
| /*attrs=*/DictAttrs(group_attrs)); |
| ffi::Array<PrimExpr> free_vars = FreeSymbolicVars(function).Map( |
| [](const tirx::Var& var) { return var.as_or_throw<PrimExpr>(); }); |
| if (!free_vars.empty()) { |
| params_.push_back(Var("tir_vars", ShapeType(free_vars))); |
| arguments_.push_back(ShapeExpr(free_vars)); |
| function = Function(/*params=*/params_, // |
| /*body=*/body, // |
| /*ret_ty=*/std::nullopt, // |
| /*is_pure=*/true, // |
| /*attrs=*/DictAttrs(group_attrs)); |
| } |
| function_ = SymbolicVarRenewMutator::Renew(function); |
| } |
| } |
| |
| /*! \brief The original bindings of the function */ |
| ffi::Array<Binding> bindings_; |
| /*! \brief The parameters of the function */ |
| ffi::Array<Var> params_; |
| /*! \brief The arguments to call the function on the caller side */ |
| ffi::Array<Expr> arguments_; |
| /*! \brief The name for the fused function */ |
| ffi::String name_hint_ = "fused"; |
| /*! \brief The constructed Relax function */ |
| ffi::Optional<Function> function_ = std::nullopt; |
| |
| private: |
| std::optional<size_t> GetOutputIndex(Var v) { |
| auto it = std::find(output_vars_.begin(), output_vars_.end(), v.get()); |
| if (it != output_vars_.end()) { |
| return std::distance(output_vars_.begin(), it); |
| } |
| return std::nullopt; |
| } |
| |
| /*! |
| * \brief Check whether the input expression is defined within this function. If not, create a new |
| * parameter for the expression. |
| * \param expr The expression to be checked |
| */ |
| void CheckDefAndUpdateParam(const Expr& expr) { |
| // If the expression has already served as an argument, no need to create another one for it. |
| if (std::find_if(arguments_.begin(), arguments_.end(), [&](const Expr& argument) { |
| return argument.same_as(expr); |
| }) != arguments_.end()) { |
| return; |
| } |
| |
| // If the expression is not a variable or is a undefined variable, it should be populated as a |
| // parameter of the relax function. |
| const auto* var = expr.as<VarNode>(); |
| if (var != nullptr && defined_vars_.count(var) == 0) { |
| Var bound_var = ffi::GetRef<Var>(var); |
| Expr bound_value = bound_var; |
| std::unordered_set<const VarNode*> visited; |
| while (const auto* current_var = bound_value.as<VarNode>()) { |
| if (!visited.insert(current_var).second) break; |
| auto it = outer_bindings_.find(ffi::GetRef<Var>(current_var)); |
| if (it == outer_bindings_.end()) break; |
| bound_value = (*it).second; |
| } |
| if (!bound_value.same_as(bound_var) && IsInlinableConstants(bound_value)) { |
| inlined_bindings_[var] = bound_value; |
| return; |
| } |
| } |
| if ((var == nullptr || defined_vars_.count(var) == 0) && |
| (lift_constant_ || !expr->IsInstance<ConstantNode>())) { |
| ffi::String name = |
| var != nullptr ? var->name : ffi::String("param_" + std::to_string(n_param_for_const_++)); |
| Type param_ty = GetType(expr); |
| if (!IsInlinableConstants(expr)) { |
| Var param(std::move(name), GetType(expr)); |
| arguments_.push_back(expr); |
| params_.push_back(param); |
| } |
| |
| // Mark the tuple parameter is partially referenced in the beginning. |
| // We will remove it from the mapping once we find it is fully referenced. |
| if (param_ty->IsInstance<TupleTypeNode>()) { |
| partially_used_tuple_params_[expr.get()] = {}; |
| tuple_param_idx_[expr.get()] = static_cast<int>(arguments_.size()) - 1; |
| } |
| } |
| } |
| |
| Expr VisitExpr(const Expr& expr) final { |
| // If the expression serves as an argument, return its correspondng parameter. |
| auto it = std::find_if(arguments_.begin(), arguments_.end(), |
| [&](const Expr& argument) { return argument.same_as(expr); }); |
| if (it != arguments_.end()) { |
| return params_[it - arguments_.begin()]; |
| } |
| if (const auto* var = expr.as<VarNode>()) { |
| if (auto inlined = inlined_bindings_.find(var); inlined != inlined_bindings_.end()) { |
| return inlined->second; |
| } |
| } |
| // Otherwise, recurse into this expression. |
| return ExprMutator::VisitExpr(expr); |
| } |
| |
| // Check if the expression is constant PrimExpr or ShapeExpr or tuple of them that can be |
| // inlined in the composite functions and excluded from args/params. |
| bool IsInlinableConstants(const Expr& expr) { |
| if (const auto* tuple = expr.as<TupleNode>()) { |
| return std::all_of(tuple->fields.begin(), tuple->fields.end(), |
| [this](const Expr& e) { return IsInlinableConstants(e); }); |
| } else if (expr.as<VarNode>() || expr.as<CallNode>()) { |
| return false; |
| } else if (auto prim_value = expr.as<PrimExpr>()) { |
| return tvm::tirx::UndefinedVars(prim_value.value()).empty(); |
| } else if (const auto* shape_expr = expr.as<ShapeExprNode>()) { |
| return std::all_of(shape_expr->values.begin(), shape_expr->values.end(), |
| [](const PrimExpr& e) { return tvm::tirx::UndefinedVars(e).empty(); }); |
| } |
| return false; |
| } |
| |
| private: |
| /*! \brief The variables defined in this function */ |
| std::unordered_set<const VarNode*> defined_vars_; |
| /*! \brief Caller variables replaced by statically inlinable bound values. */ |
| std::unordered_map<const VarNode*, Expr> inlined_bindings_; |
| /*! \brief The number of parameters reserved for constants */ |
| int n_param_for_const_ = 0; |
| /*! \brief The output vars */ |
| std::vector<const VarNode*> output_vars_; |
| /*! \brief Bindings in the caller function, used to inline static leaf expressions. */ |
| ffi::Map<Var, Expr> outer_bindings_; |
| /*! \brief Whether or not to lift bound constants to parameters */ |
| bool lift_constant_; |
| /*! \brief Mapping from tuple parameter of the function to its position index */ |
| std::unordered_map<const ExprNode*, int> tuple_param_idx_; |
| /*! |
| * \brief Mapping from partially referenced tuple parameter to the list of |
| * indices that the parameter is referred by TupleGetItem |
| */ |
| std::unordered_map<const ExprNode*, std::vector<int>> partially_used_tuple_params_; |
| }; |
| |
| /*! |
| * \brief The ExprMutator used to fuse the operators in Relax functions |
| * \details Given the partition results on the indexed-forward graph, for each group whose size is |
| * larger than one, we create a new grouped function for it, containing all bindings in that group. |
| * And we substitute the bindings in a group with a single function call to the newly created |
| * grouped function. The workflow of this ExprMutator is: for each dataflow block, |
| * - we go through the bindings one by one. For each binding, if it is in a group whose size is |
| * larger than one, we add the binding to the function of the group it is in and update the |
| * parameters and arguments of that function; |
| * - then we finalize all the grouped functions by updating their bindings using BlockBuilder; |
| * - lastly, we go through the bindings again and substitute the bindings in a group with a single |
| * call to the corresponding grouped function. |
| * |
| * After transforming a Relax function, we update the function in the IRModule. Besides, we add all |
| * newly created grouped function to the IRModule. |
| */ |
| class OperatorFusor : public ExprMutator { |
| public: |
| using Group = GraphPartitioner::Group; |
| using GroupMap = std::unordered_map<const ffi::Object*, Group*>; |
| |
| OperatorFusor(IRModule mod, const GroupMap& obj2group, bool lift_constants = true) |
| : ExprMutator(mod), |
| mod_(std::move(mod)), |
| obj2group_(obj2group), |
| lift_constants_(lift_constants) {} |
| |
| /*! |
| * \brief Construct a new operator fusor. Given the indexed-forward graph and the graph partition |
| * result on that graph, the constructor creates a mapping from each leaf AST object |
| * (e.g. parameters, variables, constants) to the group of the node corresponding to the object |
| * in the graph. |
| * \param mod The IRModule to be transformed |
| * \param graph The indexed-forward graph of the input IRModule |
| * \param groups The grouped result of the group partition on the input indexed-forward graph. |
| * \param lift_constant Whether or not to lift bound constants to parameters of the grouped |
| * function. |
| */ |
| OperatorFusor(IRModule mod, const IndexedForwardGraph& graph, const std::vector<Group*>& groups, |
| bool lift_constant = true) |
| : OperatorFusor(mod, CreateGroupMap(graph, groups), lift_constant) {} |
| |
| /*! |
| * \brief The main transformation on the IRModule |
| * \return The new IRModule after transformation |
| */ |
| IRModule Transform(const ffi::Array<ffi::String>& entry_function_names = {}) { |
| ffi::Array<GlobalVar> entry_functions; |
| if (entry_function_names.empty()) { |
| entry_functions = mod_->GetGlobalVars(); |
| } else { |
| for (const auto& name : entry_function_names) { |
| entry_functions.push_back(mod_->GetGlobalVar(name)); |
| } |
| } |
| for (const auto& gv : entry_functions) { |
| const auto& func = mod_->Lookup(gv); |
| // Only visit Relax functions with neither attr::kPrimitive nor |
| // attr::kCodegen. |
| if (func->IsInstance<relax::FunctionNode>() && !func->HasNonzeroAttr(attr::kPrimitive) && |
| !func->GetAttr<ffi::String>(attr::kCodegen).has_value()) { |
| outer_bindings_ = AnalyzeVar2Value(func); |
| auto updated_func = VisitExpr(func).as_or_throw<Function>(); |
| builder_->UpdateFunction(gv, updated_func); |
| outer_bindings_ = {}; |
| } |
| } |
| return builder_->GetContextIRModule(); |
| } |
| |
| private: |
| static GroupMap CreateGroupMap(const IndexedForwardGraph& graph, |
| const std::vector<Group*>& groups) { |
| GroupMap obj2group; |
| for (int nid = 0; nid < static_cast<int>(graph.post_dfs_order.size()); ++nid) { |
| Group* group_root = groups[nid]->FindRoot(); |
| TVM_FFI_ICHECK(group_root != nullptr); |
| TVM_FFI_ICHECK(graph.post_dfs_order[nid]->ref != nullptr); |
| obj2group[graph.post_dfs_order[nid]->ref] = group_root; |
| } |
| return obj2group; |
| } |
| |
| BindingBlock VisitBindingBlock_(const DataflowBlockNode* block) final { |
| group2func_.clear(); |
| |
| // Step 1. Collect the bindings for each grouped function. |
| CollectFuncBindings(block->bindings); |
| |
| // Step 2. Collect all group's boundary (i.e. the output vars for each group) |
| CollectFuncBoundary(block->bindings); |
| |
| // Step 3. Create the grouped function for each group. |
| for (auto& [g, creator] : group2func_) { |
| creator.CreateFunction(g->attrs); |
| } |
| |
| // Step 4. Start generating the new binding block. |
| // - For groups with single binding, we directly recurse into the binding and emit the new one. |
| // - For groups with multiple bindings, we emit the call to the grouped function only when |
| // visiting the last binding of the group, because only by doing this we don't break the |
| // dependencies among the bindings of different groups. And therefore, we will skip all but the |
| // last binding of the group. |
| builder_->BeginDataflowBlock(); |
| |
| // Preserve the original binding order when emitting TupleGetItem bindings for groups with |
| // multiple boundary outputs. Missing entries are filled when the grouped call is emitted. |
| std::unordered_map<Group*, std::vector<Var>> pending_output_remap; |
| |
| // A grouped function which returns a tuple requires attaching TupleGetItem to each element and |
| // remapping variables in earlier bindings appropriately. Thus, a binding whose value depends on |
| // some elements of a tuple from other group's function must be emitted after a call to the |
| // tuple-producing function is emitted and remapping is done. |
| // To guarantee this, we process bindings in the order of the topological sort of the group |
| // dependency relations. |
| for (const auto& binding : TopoSortByGroupDep(block->bindings)) { |
| // Case 1. If the binding is the only binding in its group, recurse into it and emit the |
| // transformed binding as usual. |
| Group* group = GetGroupFromBinding(binding); |
| if (group->num_nodes == 1 && group->attrs.empty()) { |
| VisitBinding(binding); |
| continue; |
| } |
| |
| const auto& it_creator = group2func_.find(group); |
| TVM_FFI_ICHECK(it_creator != group2func_.end()); |
| const FunctionCreator& func_info = it_creator->second; |
| |
| if (!func_info.function_.has_value()) { |
| // The function is not created yet, so we skip the binding. |
| continue; |
| } |
| const Function& func = func_info.function_.value(); |
| |
| const auto& output_vars = func_info.output_vars(); |
| if (output_vars.size() > 1 && std::find(output_vars.begin(), output_vars.end(), |
| binding->var.get()) != output_vars.end()) { |
| pending_output_remap[group].push_back(binding->var); |
| } |
| |
| // Case 2. If the binding is not the last binding of the group, we skip it. |
| if (!func_info.bindings_.back().same_as(binding)) { |
| continue; |
| } |
| |
| // Case 3. The binding is the last binding of the group. |
| const auto* var_binding = binding.as<VarBindingNode>(); |
| TVM_FFI_ICHECK(var_binding != nullptr) |
| << "The last binding of a group whose size is larger than 1 " |
| "is supposed to be a variable binding"; |
| |
| // Step a. Add the grouped function to the IRModule |
| GlobalVar gv = builder_->AddFunction(func, func_info.name_hint_); |
| |
| // Step b. Create the call to the deduplicated function, and then emit the call. |
| // A multi-output call is internal to this dataflow block, while a single-output call has the |
| // same dataflow/output status as its sole boundary variable. The last binding is only the |
| // insertion point and may itself be a dead internal binding. |
| TVM_FFI_ICHECK(!output_vars.empty()); |
| Var new_var; |
| Call call_to_emit = Call(Type::Missing(), gv, UpdateArgs(func_info.arguments_)); |
| |
| if (output_vars.size() == 1 && !output_vars[0]->IsInstance<DataflowVarNode>()) { |
| new_var = builder_->EmitOutput(call_to_emit); |
| } else { |
| new_var = builder_->Emit(call_to_emit); |
| } |
| |
| // Step c. Remap every boundary output to the corresponding result of the grouped call. |
| // FunctionCreator uses output_vars() order when it constructs a multi-output tuple. A |
| // single boundary output is returned directly, including when that output is itself a tuple. |
| if (output_vars.size() == 1) { |
| var_remap_[ffi::GetRef<Var>(output_vars[0])] = new_var; |
| continue; |
| } |
| |
| std::unordered_set<const VarNode*> remapped_outputs; |
| auto remap_output = [&](const Var& output_var) { |
| auto it = std::find(output_vars.begin(), output_vars.end(), output_var.get()); |
| TVM_FFI_ICHECK(it != output_vars.end()); |
| int index = static_cast<int>(std::distance(output_vars.begin(), it)); |
| TupleGetItem tuple_get(new_var, index); |
| if (output_var->IsInstance<DataflowVarNode>()) { |
| var_remap_[output_var] = builder_->Emit(tuple_get); |
| } else { |
| var_remap_[output_var] = builder_->EmitOutput(tuple_get); |
| } |
| remapped_outputs.insert(output_var.get()); |
| }; |
| |
| if (auto it = pending_output_remap.find(group); it != pending_output_remap.end()) { |
| for (const Var& output_var : it->second) { |
| remap_output(output_var); |
| } |
| } |
| for (const VarNode* output_var : output_vars) { |
| if (!remapped_outputs.count(output_var)) { |
| remap_output(ffi::GetRef<Var>(output_var)); |
| } |
| } |
| } |
| // Step 5. Finish the binding block generation. |
| return builder_->EndBlock(); |
| } |
| |
| /*! |
| * \brief Collect the bindings for each grouped function and update the information of the grouped |
| * function |
| * \param bindings The bindings to be collected |
| * \note The function update is done by `AppendBinding(...)` |
| */ |
| void CollectFuncBindings(const ffi::Array<Binding>& bindings) { |
| for (const Binding& binding : bindings) { |
| // If the binding is the only binding in its group, there is no need to create a new function. |
| Group* group = GetGroupFromBinding(binding); |
| if (group->num_nodes == 1 && group->attrs.empty()) { |
| continue; |
| } |
| // Add the binding to the grouped function it's in, and update the function information |
| // accordingly. |
| auto it = group2func_.try_emplace(group, lift_constants_, outer_bindings_).first; |
| it->second.AppendBinding(binding); |
| } |
| } |
| |
| void CollectFuncBoundary(const ffi::Array<Binding>& bindings) { |
| for (const Binding& binding : bindings) { |
| // Step 1. Get current binding's group |
| Group* cur_group = GetGroupFromBinding(binding); |
| |
| // Step 2. Collect all used vars in the binding value and update bondary. |
| // - If the var's group is same as the binding's, the var is defined in the same group |
| // - If the var's group is different with the binding's, the var must be the output from |
| // another group. Mark it to be the group output. |
| auto update_boundary = [this, binding, &cur_group](const Expr& e) { |
| if (e->IsInstance<VarNode>() && obj2group_.count(e.get())) { |
| const Var& used_var = e.as_or_throw<Var>(); |
| Group* producer_group = GetGroupFromVar(used_var); |
| // Only check those group defined before. |
| // Skip the vars from input or groups with single binding. |
| if (producer_group != cur_group) { |
| for (Group* depgroup : group_deps_[producer_group]) { |
| TVM_FFI_ICHECK(depgroup != cur_group) |
| << "A cyclic dependency detected between the groups " << binding->var->name |
| << " and " << used_var->name << " are in."; |
| } |
| group_deps_[cur_group].push_back(producer_group); |
| } |
| |
| if (auto producer = group2func_.find(producer_group); |
| producer_group != cur_group && producer != group2func_.end()) { |
| producer->second.AppendOutput(used_var); |
| } |
| } |
| }; |
| |
| if (const auto* var_binding = binding.as<VarBindingNode>()) { |
| PostOrderVisit(var_binding->value, update_boundary); |
| } else { |
| const auto* match_cast = binding.as<MatchCastNode>(); |
| TVM_FFI_ICHECK_NOTNULL(match_cast); |
| PostOrderVisit(match_cast->value, update_boundary); |
| } |
| } |
| } |
| |
| /*! |
| * \brief Get the group which the input binding is in |
| * \param binding The binding to be queried |
| * \return The pointer to the group which the input binding is in |
| */ |
| Group* GetGroupFromBinding(const Binding& binding) { |
| Var var = binding->var; |
| return GetGroupFromVar(var); |
| } |
| |
| /*! |
| * \brief Get the group which the input var is in |
| * \param Var The var to be queried |
| * \return The pointer to the group which the input var is in |
| */ |
| Group* GetGroupFromVar(const Var& var) { |
| const auto& it_group = obj2group_.find(var.get()); |
| TVM_FFI_ICHECK(it_group != obj2group_.end()) |
| << "Variable " << var << " could not be found in any group"; |
| Group* group = it_group->second; |
| return group->FindRoot(); |
| } |
| |
| /*! |
| * \brief Update the pre-stored arguments according to the variable remapping of the fusor, by |
| * recursing into each argument |
| * \param args The arguments to be updated |
| * \return The updated arguments |
| */ |
| ffi::Array<Expr> UpdateArgs(const ffi::Array<Expr>& args) { |
| ffi::Array<Expr> new_args; |
| new_args.reserve(args.size()); |
| for (const Expr& arg : args) { |
| new_args.push_back(VisitExpr(arg)); |
| } |
| return new_args; |
| } |
| |
| private: |
| // Topologically sort bindings according to the group dependency relations. |
| ffi::Array<Binding> TopoSortByGroupDep(const ffi::Array<Binding>& bindings) { |
| std::unordered_map<Group*, std::vector<Binding>> bindings_per_group; |
| // The order to visit groups should respect the original order of bindings as much as possible. |
| std::vector<Group*> group_order; |
| for (const auto& binding : bindings) { |
| auto g = GetGroupFromBinding(binding); |
| group_order.push_back(g); // Duplication does not matter since each group is visited once. |
| bindings_per_group[g].push_back(binding); |
| } |
| |
| std::unordered_set<Group*> visited; |
| |
| std::function<void(Group*, std::function<void(Group*)>)> dfs_visit; |
| dfs_visit = [this, &visited, &dfs_visit](Group* g, auto leaf_fun) { |
| if (!visited.count(g)) { |
| visited.insert(g); |
| for (auto dep : group_deps_[g]) { |
| dfs_visit(dep, leaf_fun); |
| } |
| leaf_fun(g); |
| } |
| }; |
| |
| ffi::Array<Binding> sorted; |
| |
| for (auto g : group_order) { |
| dfs_visit(g, [&sorted, &bindings_per_group](Group* leaf) { |
| for (const auto& binding : bindings_per_group[leaf]) { |
| sorted.push_back(binding); |
| } |
| }); |
| } |
| |
| return sorted; |
| } |
| |
| /*! \brief The IRModule. */ |
| IRModule mod_; |
| /*! \brief Internal arena. */ |
| support::Arena arena_; |
| /*! \brief The group assignment map. */ |
| GroupMap obj2group_; |
| /*! \brief Internal function information map. */ |
| std::unordered_map<Group*, FunctionCreator> group2func_; |
| /*! \brief Bindings visible while rewriting the current Relax function. */ |
| ffi::Map<Var, Expr> outer_bindings_; |
| /*! |
| * \brief A map from a group to its dependent groups, used to detect cyclic dependencies. |
| * \note Use vector so we can be deterministic, there won't be a lot of dep groups so |
| * linear search is OK. |
| */ |
| std::unordered_map<Group*, std::vector<Group*>> group_deps_; |
| /*! \brief Whether or not to lift bound constants to parameters of the grouped function. */ |
| bool lift_constants_{true}; |
| }; |
| |
| IRModule FuseOps(IRModule mod, int opt_level, size_t max_fuse_depth) { |
| support::Arena arena; |
| |
| // Step 1. Create the indexed-forward graph according to the input IRModule. |
| IndexedForwardGraph graph = GraphCreator::Create(mod, &arena); |
| |
| // Step 2. Partition the graph by applying the fusion algorithm. |
| std::vector<GraphPartitioner::Group*> groups = |
| GraphPartitioner(&arena, opt_level, max_fuse_depth, /*max_function_args=*/0).Partition(graph); |
| |
| // Step 3. Transform the IRModule by fusing the operators in accordance with the graph partition |
| // results. |
| return OperatorFusor(mod, graph, groups, /*lift_constants*/ true).Transform(); |
| } |
| |
| IRModule MakeGroupedFunctions( |
| IRModule mod, const std::unordered_map<const ffi::Object*, GraphPartitioner::Group*>& partition, |
| bool lift_constants, const ffi::Array<ffi::String>& entry_function_names) { |
| return OperatorFusor(mod, partition, lift_constants).Transform(entry_function_names); |
| } |
| |
| /*! \brief Create a "partitioning", a map from interior / leaf expr to its representative group, |
| * based on the provided pattern. The result can be passed to OperatorFusor above to fuse operations |
| * in a group and create a grouped function. |
| */ |
| class PatternBasedPartitioner : ExprVisitor { |
| public: |
| using Group = GraphPartitioner::Group; |
| using GroupMap = OperatorFusor::GroupMap; |
| using PatternCheckContext = transform::PatternCheckContext; |
| using ExprVisitor::VisitExpr_; |
| using FCheckMatch = ffi::TypedFunction<bool(const transform::PatternCheckContext&)>; |
| using FAttrsGetter = |
| ffi::TypedFunction<ffi::Map<ffi::String, ffi::Any>(const ffi::Map<ffi::String, Expr>&)>; |
| |
| static GroupMap Run(ffi::String pattern_name, DFPattern pattern, |
| ffi::Map<ffi::String, DFPattern> annotation_patterns, FCheckMatch check, |
| Expr expr, support::Arena* arena, FAttrsGetter attrs_getter) { |
| PatternBasedPartitioner part(pattern_name, pattern, annotation_patterns, check, arena, |
| attrs_getter); |
| part.VisitExpr(expr); |
| return part.group_map_; |
| } |
| |
| PatternBasedPartitioner(ffi::String pattern_name, DFPattern pattern, |
| ffi::Map<ffi::String, DFPattern> annotation_patterns, FCheckMatch check, |
| support::Arena* arena, FAttrsGetter attrs_getter) |
| : pat_name_(pattern_name), |
| pat_(pattern), |
| annotation_pat_(annotation_patterns), |
| check_(check), |
| arena_(arena), |
| attrs_getter_(attrs_getter) {} |
| |
| void VisitBindingBlock_(const DataflowBlockNode* block) final { |
| current_block_use_def_ = DataflowBlockUseDef(ffi::GetRef<DataflowBlock>(block)); |
| ExprVisitor::VisitBindingBlock_(block); |
| current_block_use_def_ = {}; |
| } |
| |
| void VisitVarDef(const Var& var) final { |
| Group* g = arena_->make<Group>(); |
| group_map_[var.get()] = g; |
| vars_in_group_[g].push_back(var); |
| } |
| |
| void VisitBinding_(const VarBindingNode* binding) final { |
| bindings_.Set(binding->var, binding->value); |
| value_to_bound_var_.Set(binding->value, binding->var); |
| ExprVisitor::VisitBinding_(binding); |
| } |
| |
| void VisitExpr_(const ConstantNode* op) final { group_map_[op] = arena_->make<Group>(); } |
| |
| void VisitBinding_(const VarBindingNode* binding, const CallNode* call) final { |
| VisitVarDef(binding->var); |
| if (auto matches_opt = ExtractMatchedExpr(pat_, ffi::GetRef<Call>(call), bindings_)) { |
| const auto& context = CreatePatternCheckContext(call, matches_opt.value()); |
| if (check_ != nullptr && !check_(context)) { |
| return; |
| } |
| |
| for (const auto& [pat, match] : matches_opt.value()) { |
| if ((pat->IsInstance<CallPatternNode>() && !match.same_as(ffi::GetRef<Call>(call))) || |
| pat->IsInstance<TupleGetItemPatternNode>()) { |
| auto g = GetGroup(match); |
| if (g && g->FindRoot()->num_nodes > 1) { |
| // This expression has already been matched to a previous pattern. |
| // If the prior matched subgraph is subsumed by the new matched one, |
| // we can safely merge them, obtaining a maximized matched subgraph enventually. |
| // Otherwise, merging them will result in an incorrect subgraph, |
| // so we keep the prior subgraph and discard the current one by directly return. |
| auto vars_in_prior_matched_graph = vars_in_group_[g]; |
| if (!GraphSubsumedInMatchedValues(vars_in_prior_matched_graph, matches_opt.value())) |
| return; |
| } |
| } |
| } |
| |
| // If a match is found, put all matching expressions into the same group. |
| // OperatorFusor also requires that the bound variable be in the same group as the RHS value. |
| // Since is_op(...) based pattern only matches against call nodes on the right hand side, |
| // we need to take care of groups corresponding to the LHS bound variables carefully. |
| |
| // In the example below, conv2d + relu pattern would match if the "call" variable in this |
| // function points to the relu op. We identify the group corresponding to "conv1", and make |
| // it the representative group for relu and conv2d on the RHS and also "lv" on the LHS. |
| |
| // with R.dataflow(): |
| // lv: R.Tensor((1, 64, 56, 56), dtype="float32") = R.nn.conv2d(...) |
| // conv1: R.Tensor((1, 64, 56, 56), dtype="float32") = R.nn.relu(lv) |
| |
| // parent_group corresponds to the group of "conv1" above. |
| auto parent_group = GetGroupForBoundVar(binding->var); |
| TVM_FFI_ICHECK(parent_group); |
| parent_group->attrs.Set(attr::kComposite, pat_name_); |
| if (attrs_getter_ != nullptr) { |
| const auto& custom_attrs = attrs_getter_(context->annotated_expr); |
| for (const auto& pair : custom_attrs) { |
| parent_group->attrs.Set(pair.first, pair.second); |
| } |
| } |
| for (const auto& [pat, match] : matches_opt.value()) { |
| // Put all matching expressions into the parent group. But we need to be careful not to |
| // merge expressions matched by a wildcard pattern, since a wildcard can match an output of |
| // the previous group. For example, when there are two back-to-back conv2d ops, the output |
| // of the first conv2d is matched to the input of the second conv2d via a wildcard pattern. |
| // But we must avoid merging the first conv2d into the group of the second conv2d. |
| if ((pat->IsInstance<CallPatternNode>() && !match.same_as(ffi::GetRef<Call>(call))) || |
| pat->IsInstance<TupleGetItemPatternNode>()) { |
| // Put the bound variable on the LHS into the same parent group. |
| AddToGroup(value_to_bound_var_[match], parent_group); |
| } |
| } |
| } |
| } |
| |
| private: |
| void AddToGroup(Expr e, Group* to) { |
| if (group_map_[e.get()] != to) { |
| --group_map_[e.get()]->num_nodes; |
| group_map_[e.get()]->parent = to; |
| vars_in_group_[to].push_back(e); |
| ++to->num_nodes; |
| } |
| } |
| |
| Group* GetGroupForBoundVar(const Var& bound_var) { |
| TVM_FFI_ICHECK(group_map_.count(bound_var.get())); |
| return group_map_[bound_var.get()]->FindRoot(); |
| } |
| |
| Group* GetGroup(const Expr& exp) { |
| if (value_to_bound_var_.count(exp) && group_map_.count(value_to_bound_var_[exp].get())) { |
| return group_map_[value_to_bound_var_[exp].get()]; |
| } |
| return nullptr; |
| } |
| |
| PatternCheckContext CreatePatternCheckContext(const CallNode* call, |
| const ffi::Map<DFPattern, Expr>& matched_result) { |
| ffi::Map<ffi::String, Expr> annotated_expr; |
| for (const auto& it : annotation_pat_) { |
| if (matched_result.count(it.second)) { |
| annotated_expr.Set(it.first, matched_result[it.second]); |
| } |
| } |
| |
| ffi::Map<Var, Expr> matched_bindings; |
| for (const auto& [pat, match] : matched_result) { |
| if (pat->IsInstance<CallPatternNode>() || pat->IsInstance<TupleGetItemPatternNode>()) { |
| matched_bindings.Set(value_to_bound_var_[match], match); |
| } |
| } |
| |
| return PatternCheckContext(ffi::GetRef<Call>(call), annotated_expr, matched_bindings, |
| current_block_use_def_, value_to_bound_var_); |
| } |
| |
| // check if a previous matched subgraph is subsumed by the current matched result |
| bool GraphSubsumedInMatchedValues(const ffi::Array<Expr>& vars_in_graph, |
| const ffi::Map<DFPattern, Expr>& matched_result) { |
| std::set<Expr, ExprIdentityLess> matched_vars; |
| for (const auto& [pat, match] : matched_result) { |
| if ((pat->IsInstance<CallPatternNode>() || pat->IsInstance<TupleGetItemPatternNode>())) |
| matched_vars.insert(value_to_bound_var_[match]); |
| } |
| |
| for (const auto var : vars_in_graph) { |
| if (matched_vars.find(var) == matched_vars.end()) return false; |
| } |
| return true; |
| } |
| |
| ffi::String pat_name_; |
| DFPattern pat_; |
| ffi::Map<ffi::String, DFPattern> annotation_pat_; |
| FCheckMatch check_; |
| support::Arena* arena_; |
| FAttrsGetter attrs_getter_; |
| ffi::Map<Var, Expr> bindings_; |
| ffi::Map<Expr, Var> value_to_bound_var_; |
| ffi::Map<Var, ffi::Array<Var>> current_block_use_def_; |
| GroupMap group_map_; |
| std::map<Group*, ffi::Array<Expr>> vars_in_group_; |
| }; |
| |
| /*! |
| * \brief Wrap each created composite function with another function, whose body consists |
| * only of a call to the composite function, and annotate the outer function with kCodegen |
| * and kGlobalSymbol attributes. |
| */ |
| class CompositeFunctionAnnotator : public ExprMutator { |
| public: |
| explicit CompositeFunctionAnnotator(IRModule mod) : ExprMutator(mod) {} |
| using ExprMutator::VisitExpr_; |
| |
| IRModule Run() { |
| auto mod = builder_->GetContextIRModule(); |
| for (const auto& gv : mod->GetGlobalVars()) { |
| auto it = mod->functions.find(gv); |
| // Note that the fusion pass may have already removed the function. |
| if (it == mod->functions.end()) { |
| continue; |
| } |
| const auto& base_func = (*it).second; |
| if (const auto* func = base_func.as<FunctionNode>()) { |
| if (func->GetAttr<ffi::String>(attr::kComposite).has_value() || |
| func->GetAttr<ffi::String>(attr::kCodegen).has_value()) { |
| continue; |
| } |
| |
| auto new_body = VisitWithNewScope(func->body, func->params); |
| if (!new_body.same_as(func->body)) { |
| auto new_func = Function(func->params, new_body, func->ret_ty, func->is_pure, func->attrs, |
| func->span); |
| builder_->UpdateFunction(gv, new_func); |
| } |
| } |
| } |
| return builder_->GetContextIRModule(); |
| } |
| |
| Expr VisitExpr_(const CallNode* call_node) final { |
| if (auto const* gvar = call_node->op.as<GlobalVarNode>()) { |
| if (auto it = gvar_map_.find(gvar); it != gvar_map_.end()) { |
| return Call(Type::Missing(), it->second, call_node->args); |
| } |
| auto func = builder_->GetContextIRModule()->Lookup(ffi::GetRef<GlobalVar>(gvar)); |
| if (auto composite_name = func->GetAttr<ffi::String>(attr::kComposite)) { |
| auto new_func = VisitExpr(func).as_or_throw<Function>(); |
| auto codegen_name = GetCodegenName(composite_name.value()); |
| auto gsymbol = gvar->name_hint + "_" + codegen_name; |
| new_func = WithAttrs(new_func, |
| {{attr::kCodegen, codegen_name}, {tvm::attr::kGlobalSymbol, gsymbol}}); |
| new_func = WithoutAttr(std::move(new_func), tvm::relax::attr::kPrimitive); |
| builder_->GetContextIRModule()->Remove(ffi::GetRef<GlobalVar>(gvar)); |
| auto new_gvar = builder_->AddFunction(new_func, gsymbol); |
| gvar_map_[gvar] = new_gvar; |
| return Call(Type::Missing(), new_gvar, call_node->args); |
| } |
| } |
| return ExprMutator::VisitExpr_(call_node); |
| } |
| |
| Expr VisitExpr_(const FunctionNode* func_node) final { |
| Function f_inner = ExprMutator::VisitExpr_(func_node).as_or_throw<Function>(); |
| |
| if (!func_node->GetAttr<ffi::String>(attr::kComposite)) { |
| // This lambda function doesn't have `attr::kComposite`, so it |
| // was not produced by FuseOps. |
| return f_inner; |
| } |
| |
| f_inner = WithoutAttr(std::move(f_inner), tvm::relax::attr::kPrimitive); |
| |
| ffi::Array<Var> param_vars; |
| ffi::Array<Expr> params; |
| |
| for (auto v : func_node->params) { |
| Var new_v(v->name, GetType(v)); |
| param_vars.push_back(new_v); |
| params.push_back(new_v); |
| } |
| |
| // We cannot delegate to `ExprMutator::VisitExpr_(const FunctionNode*)` at this point, as it |
| // would recursively visit the Call node. However, we are still required to generate |
| // well-formed Relax IR. As a result, we need to build the SeqExpr ourselves. |
| Var local_func_var("local_func", GetType(f_inner)); |
| Var output_var("output", f_inner->ret_ty); |
| SeqExpr new_body({BindingBlock({ |
| VarBinding(local_func_var, f_inner), |
| VarBinding(output_var, Call(Type::Missing(), local_func_var, params)), |
| })}, |
| output_var); |
| |
| // pure if the inner func is pure (no need to force purity if it's forced for the inner func) |
| return Function(param_vars, new_body, func_node->ret_ty, f_inner->is_pure); |
| } |
| |
| private: |
| /*! \brief A map from old global vars to their replacements. */ |
| std::unordered_map<const GlobalVarNode*, GlobalVar> gvar_map_; |
| }; |
| |
| IRModule FuseOpsByPattern(const tvm::ffi::Array<transform::FusionPattern>& patterns, IRModule mod, |
| bool bind_constants, bool annotate_codegen, |
| ffi::Array<ffi::String> entry_function_names) { |
| support::Arena arena; |
| |
| for (const auto& pattern : patterns) { |
| ffi::Array<Function> entry_functions; |
| if (entry_function_names.size()) { |
| for (const auto& name : entry_function_names) { |
| auto gv = mod->GetGlobalVar(name); |
| auto func = mod->Lookup(gv); |
| TVM_FFI_ICHECK(func->IsInstance<FunctionNode>()) |
| << "Entry function must be a relax function"; |
| entry_functions.push_back(func.as_or_throw<Function>()); |
| } |
| } else { |
| for (const auto& gv : mod->GetGlobalVars()) { |
| const auto& base_func = mod->Lookup(gv); |
| if (base_func->IsInstance<tirx::PrimFuncNode>()) { |
| continue; |
| } |
| const FunctionNode* function = base_func.as<FunctionNode>(); |
| if (function->GetAttr<bool>(attr::kPrimitive).value_or(false) || |
| function->GetAttr<ffi::String>(attr::kComposite).has_value() || |
| function->GetAttr<ffi::String>(attr::kCodegen).has_value()) { |
| continue; |
| } |
| entry_functions.push_back(base_func.as_or_throw<Function>()); |
| } |
| } |
| OperatorFusor::GroupMap group_map; |
| for (const auto& func : entry_functions) { |
| auto map = PatternBasedPartitioner::Run( |
| pattern->name, pattern->pattern, pattern->annotation_patterns, |
| pattern->check.value_or(nullptr), func, &arena, pattern->attrs_getter.value_or(nullptr)); |
| for (const auto& [key, value] : map) { |
| TVM_FFI_CHECK(!group_map.count(key), ValueError) |
| << "IRModule is invalid. " |
| << "The object " << ffi::GetRef<ffi::ObjectRef>(key) |
| << " appears in multiple partitions, " |
| << "which can occur when the IRModule was not single-site assignment"; |
| group_map.insert({key, value}); |
| } |
| } |
| mod = MakeGroupedFunctions(mod, group_map, /*lift_constants*/ !bind_constants, |
| entry_function_names); |
| } |
| if (annotate_codegen) { |
| return CompositeFunctionAnnotator(mod).Run(); |
| } |
| return mod; |
| } |
| |
| namespace transform { |
| |
| FusionPattern::FusionPattern(ffi::String name, DFPattern pattern, |
| ffi::Map<ffi::String, DFPattern> annotation_patterns, |
| ffi::Optional<ffi::Function> check, |
| ffi::Optional<ffi::Function> attrs_getter) { |
| ffi::ObjectPtr<FusionPatternNode> n = ffi::make_object<FusionPatternNode>(); |
| n->name = std::move(name); |
| n->pattern = std::move(pattern); |
| n->annotation_patterns = std::move(annotation_patterns); |
| n->check = check; |
| n->attrs_getter = attrs_getter; |
| data_ = std::move(n); |
| } |
| |
| TVM_FFI_STATIC_INIT_BLOCK() { |
| namespace refl = tvm::ffi::reflection; |
| refl::GlobalDef().def( |
| "relax.transform.FusionPattern", |
| [](ffi::String name, DFPattern pattern, ffi::Map<ffi::String, DFPattern> annotation_patterns, |
| ffi::Optional<ffi::Function> check, ffi::Optional<ffi::Function> attrs_getter) { |
| return FusionPattern(name, pattern, annotation_patterns, check, attrs_getter); |
| }); |
| } |
| |
| PatternCheckContext::PatternCheckContext(Expr matched_expr, |
| ffi::Map<ffi::String, Expr> annotated_expr, |
| ffi::Map<Var, Expr> matched_bindings, |
| ffi::Map<Var, ffi::Array<Var>> var_usages, |
| ffi::Map<Expr, Var> value_to_bound_var) { |
| ffi::ObjectPtr<PatternCheckContextNode> n = ffi::make_object<PatternCheckContextNode>(); |
| n->matched_expr = std::move(matched_expr); |
| n->annotated_expr = std::move(annotated_expr); |
| n->matched_bindings = std::move(matched_bindings); |
| n->var_usages = std::move(var_usages); |
| n->value_to_bound_var = std::move(value_to_bound_var); |
| data_ = std::move(n); |
| } |
| |
| Pass FuseOps(int fuse_opt_level) { |
| auto pass_func = // |
| [=](IRModule m, PassContext pc) { |
| int opt_level = fuse_opt_level == -1 ? pc->opt_level : fuse_opt_level; |
| auto max_fuse_depth = pc->GetConfig<int64_t>("relax.FuseOps.max_depth", kMaxFusedOps); |
| return relax::FuseOps(m, opt_level, static_cast<size_t>(max_fuse_depth.value())); |
| }; |
| return CreateModulePass(/*pass_function=*/pass_func, // |
| /*opt_level=*/0, // |
| /*name=*/"FuseOps", // |
| /*required=*/{}); |
| } |
| |
| TVM_FFI_STATIC_INIT_BLOCK() { |
| namespace refl = tvm::ffi::reflection; |
| refl::GlobalDef().def("relax.transform.FuseOps", FuseOps); |
| } |
| |
| Pass FuseOpsByPattern(const tvm::ffi::Array<FusionPattern>& patterns, bool bind_constants, |
| bool annotate_codegen, const ffi::Array<ffi::String>& entry_function_names) { |
| auto pass_func = // |
| [=](IRModule m, PassContext pc) { |
| return relax::FuseOpsByPattern(patterns, m, bind_constants, annotate_codegen, |
| entry_function_names); |
| }; |
| return CreateModulePass(/*pass_function=*/pass_func, // |
| /*opt_level=*/0, // |
| /*name=*/"FuseOpsByPattern", // |
| /*required=*/{}); |
| } |
| |
| TVM_FFI_STATIC_INIT_BLOCK() { |
| namespace refl = tvm::ffi::reflection; |
| refl::GlobalDef().def("relax.transform.FuseOpsByPattern", FuseOpsByPattern); |
| } |
| |
| } // namespace transform |
| |
| } // namespace relax |
| } // namespace tvm |