blob: 2f2e0895e76668a5d6a4c9f1290fc3f4bd2b2439 [file]
/*
* 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_