| /* |
| * 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/utils.h |
| * \brief Additional utility classes and functions for working with the Relax IR. |
| */ |
| #ifndef TVM_RELAX_TRANSFORM_UTILS_H_ |
| #define TVM_RELAX_TRANSFORM_UTILS_H_ |
| |
| #include <builtin_fp16.h> |
| #include <tvm/ffi/cast.h> |
| #include <tvm/ir/module.h> |
| #include <tvm/relax/expr.h> |
| #include <tvm/relax/expr_functor.h> |
| #include <tvm/tirx/expr_functor.h> |
| |
| #include <algorithm> |
| #include <string> |
| #include <unordered_map> |
| #include <utility> |
| #include <vector> |
| |
| #include "../analysis/graph_partitioner.h" |
| #include "../op/nn/convolution.h" |
| #include "../op/nn/nn.h" |
| #include "../op/nn/pooling.h" |
| #include "../op/tensor/binary.h" |
| #include "../op/tensor/create.h" |
| #include "../op/tensor/datatype.h" |
| #include "../op/tensor/index.h" |
| #include "../op/tensor/linear_algebra.h" |
| #include "../op/tensor/manipulate.h" |
| #include "../op/tensor/search.h" |
| #include "../op/tensor/set.h" |
| #include "../op/tensor/statistical.h" |
| #include "../op/tensor/ternary.h" |
| #include "../op/tensor/unary.h" |
| |
| namespace tvm { |
| namespace relax { |
| |
| /*! |
| * \brief A simple wrapper around ExprFunctor for a single argument case. |
| * The result of visit is memoized. |
| */ |
| template <typename OutputType> |
| class MemoizedExprTranslator : public ExprFunctor<OutputType(const Expr&)> { |
| using BaseFunctor = ExprFunctor<OutputType(const Expr&)>; |
| |
| public: |
| /*! \brief virtual destructor */ |
| virtual ~MemoizedExprTranslator() {} |
| |
| /*! |
| * \brief The memoized call. |
| * \param n The expression node. |
| * \return The result of the call |
| */ |
| virtual OutputType VisitExpr(const Expr& n) { |
| TVM_FFI_ICHECK(n.defined()); |
| auto it = memo_.find(n); |
| if (it != memo_.end()) { |
| return it->second; |
| } |
| auto res = BaseFunctor::VisitExpr(n); |
| memo_[n] = res; |
| return res; |
| } |
| |
| virtual OutputType VisitExpr_(const VarNode* vn) { |
| TVM_FFI_ICHECK(memo_.count(ffi::GetRef<Expr>(vn))); |
| return memo_[ffi::GetRef<Expr>(vn)]; |
| } |
| |
| virtual OutputType VisitExpr_(const DataflowVarNode* vn) { |
| return VisitExpr_(static_cast<const VarNode*>(vn)); |
| } |
| |
| virtual OutputType VisitBinding_(const VarBindingNode* binding) { |
| TVM_FFI_ICHECK_EQ(memo_.count(binding->var), 0); |
| auto v = VisitExpr(binding->value); |
| memo_[binding->var] = v; |
| return v; |
| } |
| |
| protected: |
| /*! \brief Internal map used for memoization. */ |
| std::unordered_map<Expr, OutputType, ffi::ObjectPtrHash, ffi::ObjectPtrEqual> memo_; |
| }; |
| |
| /*! |
| * \brief Dead code elimination |
| * Currently it removes: |
| * 1. Unused local VarBindings in a DataflowBlock. |
| * The used var set is set to empty at the beginning of each DataflowBlock. |
| * We reverse scan the DataflowBlock, if a VarBinding |
| * - bindings to a dataflowvar, or |
| * - is used in the used var set |
| * We keep it and add its var to the used var set. Otherwise, we remove it. |
| * 2. Unused Relax functions in the module. |
| * We detect the call chain from the entry function, and remove all unused functions. |
| * \param mod The target module |
| * \param entry_functions list of entry functions |
| * \return The updated module. |
| */ |
| TVM_DLL IRModule DeadCodeElimination(const IRModule& mod, ffi::Array<ffi::String> entry_funcs); |
| |
| /*! |
| * \brief Get the external symbol of the Relax function name. |
| * |
| * \param func The provided function. |
| * \return An external symbol. |
| */ |
| inline std::string GetExtSymbol(const Function& func) { |
| const auto name_node = func->GetAttr<ffi::String>(tvm::attr::kGlobalSymbol); |
| TVM_FFI_ICHECK(name_node.has_value()) << "Fail to retrieve external symbol."; |
| return std::string(name_node.value()); |
| } |
| |
| /*! |
| * \brief Fuse ops or functions according to the given partition, and grouped them into a new |
| * function. |
| * |
| * \param mod The input module. |
| * \param partition A mapping from a subexpression to the containing group. |
| * \param lift_constants Whether or not to lift bound constants to parameters of the |
| * grouped function. |
| * \param entry_function_names The names of the entry functions. |
| * \return A new module containing grouped functions. |
| */ |
| IRModule MakeGroupedFunctions( |
| IRModule mod, const std::unordered_map<const ffi::Object*, GraphPartitioner::Group*>& partition, |
| bool lift_constants = true, const ffi::Array<ffi::String>& entry_function_names = {}); |
| |
| /*! |
| * \brief Check if the given Type is a scalar tensor. The ty should be an instance of |
| * TensorType; its shape must be ShapeExpr. |
| * \param ty The Type to be checked. |
| * \return true if the given Type is a scalar tensor. |
| */ |
| bool IsScalarTensor(const Type& ty); |
| |
| /*! |
| * \brief Check if the given expr is a scalar tensor. Now the shape of the tensor expr must be |
| * ShapeExpr. |
| * \param expr The expr to be checked. |
| * \return true if the given expr is a scalar tensor. |
| */ |
| bool IsScalarTensor(const Expr& expr); |
| |
| /*! |
| * \brief Check if the given Type is a nested tensor Type satisfying the given |
| * condition f_condition. |
| * \param ty The Type to be checked. |
| * \param f_condition The condition function for each leaf Type with signature |
| * `bool f_condition(TensorType)`. |
| * \tparam FType The condition function type. |
| * \return true if the given Type is a nested tensor satisfying the given f_condition. |
| */ |
| template <typename FType> |
| bool IsNestedTensorConditioned(const Type& ty, FType f_condition) { |
| if (const auto* tensor_ty = ty.as<TensorTypeNode>()) { |
| return f_condition(ffi::GetRef<TensorType>(tensor_ty)); |
| } else if (const auto* tuple_ty = ty.as<TupleTypeNode>()) { |
| return !std::any_of(tuple_ty->fields.begin(), tuple_ty->fields.end(), [&](const Type& field) { |
| return !IsNestedTensorConditioned(field, f_condition); |
| }); |
| } |
| return false; |
| } |
| |
| /*! |
| * \brief Check if the given Type is a nested tensor. |
| * \param ty The Type to be checked. |
| * \return true if the given Type is a nested tensor. |
| */ |
| bool IsNestedTensor(const Type& ty); |
| |
| /*! |
| * \brief Check if the given expr is a nested tensor. |
| * \param expr The expr to be checked. |
| * \return true if the given expr is a nested tensor. |
| */ |
| bool IsNestedTensor(const Expr& expr); |
| |
| // TODO(@bohan): implements some postorder function accepts a visitor closure |
| class VarReplacer : public ExprMutator { |
| public: |
| using VarMap = std::unordered_map<Var, Var, ffi::ObjectPtrHash, ffi::ObjectPtrEqual>; |
| |
| explicit VarReplacer(const VarMap& var_remap) : var_remap_(var_remap) {} |
| |
| static Expr Replace(const Expr& expr, const VarMap& var_remap) { |
| VarReplacer replacer(var_remap); |
| return replacer(expr); |
| } |
| |
| private: |
| Expr VisitExpr_(const VarNode* op) final { |
| Var var = ffi::GetRef<Var>(op); |
| auto it = var_remap_.find(var); |
| return it == var_remap_.end() ? var : it->second; |
| } |
| |
| const VarMap& var_remap_; |
| }; |
| |
| /*! |
| * \brief Renew the definition of dependent primitive vars in Relax. |
| * \details This mutator is used to prevent the same primitive Var object from being defined in |
| * different functions, which is malformed. |
| */ |
| class SymbolicVarRenewMutator : public ExprMutator { |
| public: |
| static Function Renew(const Function& function) { |
| SymbolicVarRenewMutator mutator; |
| return mutator.VisitExpr(function).as_or_throw<Function>(); |
| } |
| SymbolicVarRenewMutator() : SymbolicVarRenewMutator(false) {} |
| |
| protected: |
| explicit SymbolicVarRenewMutator(bool renew_all_var_definitions) |
| : renew_all_var_definitions_(renew_all_var_definitions) {} |
| |
| using relax::ExprMutator::VisitExpr; |
| using relax::ExprMutator::VisitExpr_; |
| |
| static Var CopyVar(const VarNode* op, Type ty) { |
| ffi::Optional<Type> ty_annotation = ty.IsMissing() ? std::nullopt : ffi::Optional<Type>(ty); |
| if (op->IsInstance<DataflowVarNode>()) { |
| return DataflowVar(op->name, std::move(ty_annotation), op->span); |
| } |
| return Var(op->name, std::move(ty_annotation), op->span); |
| } |
| |
| Type RenewType(const VarNode* op) { |
| return op->ty.IsMissing() ? op->ty : this->VisitExprDepTypeField(op->ty); |
| } |
| |
| Var RenewVarDefinition(const VarNode* op) { |
| Var old_var = ffi::GetRef<Var>(op); |
| if (auto it = var_remap_.find(old_var); it != var_remap_.end()) { |
| return it->second; |
| } |
| |
| Type new_ty = RenewType(op); |
| bool is_dataflow = op->IsInstance<DataflowVarNode>(); |
| bool renew = renew_all_var_definitions_ || (!is_dataflow && op->ty.as<PrimTypeNode>()) || |
| !new_ty.same_as(op->ty); |
| if (!renew) { |
| return old_var; |
| } |
| |
| Var renewed = CopyVar(op, std::move(new_ty)); |
| var_remap_[old_var] = renewed; |
| return renewed; |
| } |
| |
| Expr VisitExpr_(const VarNode* op) final { |
| Var old_var = ffi::GetRef<Var>(op); |
| if (auto it = var_remap_.find(old_var); it != var_remap_.end()) { |
| return it->second; |
| } |
| if (!op->ty.as<PrimTypeNode>()) { |
| return ExprMutator::VisitExpr_(op); |
| } |
| |
| Var renewed = CopyVar(op, op->ty); |
| var_remap_[old_var] = renewed; |
| return renewed; |
| } |
| |
| Expr VisitExpr_(const DataflowVarNode* op) final { |
| Var old_var = ffi::GetRef<Var>(op); |
| if (auto it = var_remap_.find(old_var); it != var_remap_.end()) { |
| return it->second; |
| } |
| if (!renew_all_var_definitions_) { |
| return old_var; |
| } |
| |
| Var renewed = CopyVar(op, op->ty); |
| var_remap_[old_var] = renewed; |
| return renewed; |
| } |
| |
| Var VisitVarDef_(const VarNode* op) override { return RenewVarDefinition(op); } |
| |
| Var VisitVarDef_(const DataflowVarNode* op) override { return RenewVarDefinition(op); } |
| |
| Expr VisitExpr_(const FunctionNode* op) final { |
| tvm::ffi::Array<Var> params; |
| bool all_params_unchanged = true; |
| for (Var param : op->params) { |
| Var new_param = this->VisitVarDef(param); |
| params.push_back(new_param); |
| if (!param.same_as(new_param)) { |
| var_remap_[param] = new_param; |
| all_params_unchanged = false; |
| } |
| } |
| |
| Expr body = this->VisitWithNewScope(op->body, params); |
| |
| if (all_params_unchanged && body.same_as(op->body)) { |
| return ffi::GetRef<Expr>(op); |
| } else { |
| auto new_ret_ty = this->VisitExprDepTypeField(op->ret_ty); |
| return Function(params, body, new_ret_ty, op->is_pure, op->attrs); |
| } |
| } |
| |
| bool renew_all_var_definitions_{false}; |
| }; |
| |
| /*! |
| * \brief Copy a function while renewing every Var object, including dependent primitive uses. |
| * \details All variables that are bound inside the original function would be copied to satisfy |
| * the restriction in the well-formed check: Variables in Relax must be bound exactly once. |
| */ |
| class FunctionCopier : public SymbolicVarRenewMutator { |
| public: |
| FunctionCopier() : SymbolicVarRenewMutator(true) {} |
| Function Copy(Function func) { return VisitExpr(func).as_or_throw<Function>(); } |
| ffi::Map<Var, Var> GetVarMap() { |
| ffi::Map<Var, Var> result; |
| for (const auto& [old_var, new_var] : var_remap_) { |
| result.Set(old_var, new_var); |
| } |
| return result; |
| } |
| }; |
| |
| /*! |
| * \brief Create a Constant with a scalar |
| * |
| * \param dtype The data type. |
| * \param value The value of the scalar. |
| * \return A Constant. |
| */ |
| template <typename T> |
| inline Constant MakeConstantScalar(T value, DLDataType dtype) { |
| runtime::Tensor arr = runtime::Tensor::Empty({}, dtype, {kDLCPU, 0}); |
| if (dtype == DLDataType{kDLFloat, 32, 1}) { |
| *static_cast<float*>(arr->data) = static_cast<float>(value); |
| } else if (dtype == DLDataType{kDLFloat, 64, 1}) { |
| *static_cast<double*>(arr->data) = static_cast<double>(value); |
| } else if (dtype == DLDataType{kDLInt, 32, 1}) { |
| *static_cast<int32_t*>(arr->data) = static_cast<int32_t>(value); |
| } else if (dtype == DLDataType{kDLInt, 64, 1}) { |
| *static_cast<int64_t*>(arr->data) = static_cast<int64_t>(value); |
| } else if (dtype == DLDataType{kDLBool, 8, 1}) { |
| *static_cast<bool*>(arr->data) = static_cast<bool>(value); |
| } else if (dtype == DLDataType{kDLUInt, 8, 1}) { |
| *static_cast<uint8_t*>(arr->data) = static_cast<uint8_t>(value); |
| } else if (dtype == DLDataType{kDLUInt, 16, 1}) { |
| *static_cast<uint16_t*>(arr->data) = static_cast<uint16_t>(value); |
| } else if (dtype == DLDataType{kDLUInt, 32, 1}) { |
| *static_cast<uint32_t*>(arr->data) = static_cast<uint32_t>(value); |
| } else if (dtype == DLDataType{kDLUInt, 64, 1}) { |
| *static_cast<uint64_t*>(arr->data) = static_cast<uint64_t>(value); |
| } else if (dtype == DLDataType{kDLInt, 8, 1}) { |
| *static_cast<int8_t*>(arr->data) = static_cast<int8_t>(value); |
| } else if (dtype == DLDataType{kDLInt, 16, 1}) { |
| *static_cast<int16_t*>(arr->data) = static_cast<int16_t>(value); |
| } else if (dtype == DLDataType{kDLInt, 32, 1}) { |
| *static_cast<int32_t*>(arr->data) = static_cast<int32_t>(value); |
| } else if (dtype == DLDataType{kDLInt, 64, 1}) { |
| *static_cast<int64_t*>(arr->data) = static_cast<int64_t>(value); |
| } else if (dtype == DLDataType{kDLFloat, 16, 1}) { |
| // convert to float16 storage is uint16_t |
| *static_cast<uint16_t*>(arr->data) = |
| __truncXfYf2__<float, uint32_t, 23, uint16_t, uint16_t, 10>(static_cast<float>(value)); |
| } else if (dtype == DLDataType{kDLBfloat, 16, 1}) { |
| // convert to bfloat16 storage is uint16_t |
| *static_cast<uint16_t*>(arr->data) = |
| __truncXfYf2__<float, uint32_t, 23, uint16_t, uint16_t, 7>(static_cast<float>(value)); |
| } else { |
| TVM_FFI_THROW(InternalError) << "Unsupported dtype " << dtype; |
| } |
| return Constant(arr); |
| } |
| |
| inline ffi::Array<int64_t> GetOrderedPositiveAxes(const ffi::Array<int64_t>& axes, int ndim) { |
| std::vector<int64_t> ret; |
| ret.reserve(axes.size()); |
| for (int64_t axis_val : axes) { |
| if (axis_val < 0) { |
| axis_val += ndim; |
| } |
| TVM_FFI_ICHECK(axis_val >= 0 && axis_val < ndim) |
| << "axis " << axis_val << " is out of bounds for array of " |
| << "dimension " << ndim; |
| ret.push_back(axis_val); |
| } |
| std::sort(ret.begin(), ret.end()); |
| ffi::Array<int64_t> result; |
| result.reserve(ret.size()); |
| for (int64_t x : ret) result.push_back(x); |
| return result; |
| } |
| |
| inline ffi::String GetCodegenName(const std::string& composite_name) { |
| auto delim_pos = composite_name.find("."); |
| TVM_FFI_ICHECK(delim_pos != std::string::npos) |
| << "The pattern name for a composite function should " |
| "start with a compiler name followed by period."; |
| return composite_name.substr(0, delim_pos); |
| } |
| |
| inline int GetDeviceIndexByScope(const IRModule& mod, const ffi::String& scope) { |
| if (mod->global_infos.find("vdevice") == mod->global_infos.end()) { |
| return 0; |
| } |
| ffi::Array<GlobalInfo> vdevices = mod->global_infos["vdevice"]; |
| for (int i = 0; i < static_cast<int>(vdevices.size()); ++i) { |
| if (scope == vdevices[i].as<VDevice>().value()->memory_scope) { |
| return i; |
| } |
| } |
| return 0; |
| } |
| |
| inline int GetDeviceIndex(const IRModule& mod, const VDevice& vdevice) { |
| ffi::Array<GlobalInfo> vdevices = mod->global_infos["vdevice"]; |
| for (int i = 0; i < static_cast<int>(vdevices.size()); ++i) { |
| if (vdevices[i].same_as(vdevice)) { |
| return i; |
| } |
| } |
| TVM_FFI_THROW(InternalError) << "The vdevice is not in the ir_module."; |
| return -1; |
| } |
| |
| inline ffi::Optional<VDevice> GetGlobalVDevice(const IRModule& mod, const int index) { |
| ffi::Optional<VDevice> ret; |
| if (mod->global_infos.find("vdevice") != mod->global_infos.end()) { |
| ffi::Array<GlobalInfo> vdevices = mod->global_infos["vdevice"]; |
| if (index < static_cast<int>(vdevices.size())) { |
| ret = vdevices[index].as<VDevice>(); |
| } |
| } |
| return ret; |
| } |
| |
| /* \brief Eliminate common subexpressions |
| * |
| * Utility for simplifying relax expressions by removing common |
| * subexpressions. |
| * |
| * \param expr The expression to be updated |
| * |
| * \param call_only If true, only eliminate relax::Call nodes. If |
| * false, eliminate any common subexpressions. |
| * |
| * \ret The updated expression |
| */ |
| Expr EliminateCommonSubexpr(const Expr& expr, bool call_only = false); |
| |
| /* \brief Remove use of trivial bindings |
| * |
| * Utility for simplifying relax expressions by folding var bindings |
| * and match shape nodes. May include other forms of simplification |
| * in the future. Ideally should be used before constant folding and |
| * eliminating unused bindings. |
| * |
| * \param expr The expression to be canonicalized |
| * |
| * \ret The canonicalized expression |
| */ |
| Expr CanonicalizeBindings(Expr expr); |
| |
| /* \brief Remove use of trivial bindings |
| * |
| * Utility for converting from individual model parameters to a single |
| * parameter with a tuple of parameters. If the `kNumInput` attribute |
| * is absent, no model parameters are present, so no updates are made. |
| * |
| * \param func The function to be updated. |
| * |
| * \param param_tuple_name The name of the tuple parameter. If |
| * unspecified, defaults to "model_params" |
| * |
| * \ret The updated function. |
| */ |
| Function BundleModelParams(const Function& func, |
| ffi::Optional<ffi::String> param_tuple_name = std::nullopt); |
| |
| /*! \brief Compose two functions |
| * |
| * Given two functions `func_a` and `func_b`, produce `func_c` such |
| * that `func_c(x)` is equivalent to `func_b(func_a(x))`. |
| * |
| * If the output if `func_a` is not usable as the input of `func_b`, |
| * an error will be raised. |
| * |
| * \param func_a The first function to be composed. |
| * \param func_b The second function to be composed. |
| * \return The composed function |
| */ |
| TVM_DLL Function ComposeFunctions(Function func_a, Function func_b); |
| |
| } // namespace relax |
| } // namespace tvm |
| |
| #endif // TVM_RELAX_TRANSFORM_UTILS_H_ |