blob: 350bb66c90de8c4e8ba942585e587b423b8ce75a [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 tvm/relax/transform/lambda_lift.cc
* \brief Lift local functions into global functions.
*/
#include <tvm/ffi/cast.h>
#include <tvm/ffi/error.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/relax/analysis.h>
#include <tvm/relax/expr.h>
#include <tvm/relax/expr_functor.h>
#include <tvm/relax/transform.h>
#include <iostream>
#include <vector>
namespace tvm {
namespace relax {
namespace {
/* \brief Collect names of functions to be lifted out */
class LambdaNameCollector : ExprVisitor {
public:
static std::unordered_map<const FunctionNode*, ffi::String> Collect(const IRModule& mod) {
LambdaNameCollector visitor;
for (const auto& [gvar, base_func] : mod->functions) {
visitor.previous_global_vars_.insert(gvar->name_hint);
}
for (const auto& [gvar, base_func] : mod->functions) {
if (auto func = base_func.as<Function>()) {
visitor.name_stack_.push_back(gvar->name_hint);
visitor(func.value());
visitor.name_stack_.pop_back();
}
}
return visitor.Finalize();
}
private:
void VisitBinding_(const VarBindingNode* binding, const FunctionNode* func) override {
if (auto opt = func->GetAttr<ffi::String>(tvm::attr::kGlobalSymbol)) {
ffi::String public_name = opt.value();
// If a kGlobalSymbol exists, we must use the name exactly as it
// appears, with no modifications. Because these errors would
// be raised from deep within an optimization pipeline, but
// depends on small annotation changes from a user's initial
// model definition, they are intentionally verbose to
// (hopefully) provide sufficient context to a user encountering
// the error.
TVM_FFI_ICHECK(!previous_global_vars_.count(public_name))
<< "Function " << name_stack_.front() << " contains a lambda with kGlobalSymbol (\""
<< tvm::attr::kGlobalSymbol << "\" attribute of \"" << public_name << "\". "
<< "However, the module already contains a GlobalVar with this name. "
<< "If present, the kGlobalSymbol attribute must match the name of the GlobalVar, "
<< "and GlobalVar names must be unique across an IRModule. "
<< "Lifting the " << public_name << " function out of " << name_stack_.front()
<< " would require violating one of these two conditions.";
auto it = new_public_names_.find(public_name);
TVM_FFI_ICHECK(it == new_public_names_.end())
<< "Function " << name_stack_.front() << " contains a lambda with kGlobalSymbol (\""
<< tvm::attr::kGlobalSymbol << "\" attribute of \"" << public_name << "\". "
<< "However, the function " << it->second.front()
<< " also contains a lambda with the same value for kGlobalSymbol. "
<< "If present, the kGlobalSymbol attribute must match the name of the GlobalVar, "
<< "and GlobalVar names must be unique across an IRModule. "
<< "Lifting the " << public_name << " function out of both " << name_stack_.front()
<< " and " << it->second.front()
<< " would require violating one of these two conditions.";
new_public_names_.insert({public_name, name_stack_});
lifted_with_global_symbol_.insert({func, public_name});
}
name_stack_.push_back(binding->var->name);
lambda_location_.insert({func, name_stack_});
ExprVisitor::VisitBinding_(binding, func);
name_stack_.pop_back();
}
// De-duplication of collected names
std::unordered_map<const FunctionNode*, ffi::String> Finalize() const {
// The functions which still must be assigned a name
std::unordered_map<const FunctionNode*, ffi::Array<ffi::String>> remaining_to_name =
lambda_location_;
// Collecting the functions that now have a name.
std::unordered_map<const FunctionNode*, ffi::String> lifted_names;
// A lookup for names that are unavailable for use.
std::unordered_set<ffi::String> unavailable_names = previous_global_vars_;
// A helper function to generate de-duplicated names. The
// `proposed_name_generation_func` should be a function with
// signature:
//
// ffi::Optional<ffi::String> func(const FunctionNode*, const ffi::Array<ffi::String>&)
//
// The first argument will be the lambda function being lifted.
// The second argument will be the nested location where that
// lambda function was found. The function should return the
// proposed name for the lifted lambda function. The proposed
// name will be accepted if it does not conflict with any previous
// names, and is unique for all lambda functions being lifted.
//
// This helper function is used to apply several different schemes
// to generate the name of the lifted lambda function. The
// overall goal is to provide names that are unique (required by
// IRModule), deterministic (required for unit testing), and
// human-readable.
auto attempt_name_generation = [&](const auto& proposed_name_generation_func) {
if (remaining_to_name.empty()) {
return;
}
std::unordered_map<ffi::String, const FunctionNode*> new_names;
for (const auto& [func, location] : remaining_to_name) {
if (ffi::Optional<ffi::String> opt_proposed_name =
proposed_name_generation_func(func, location)) {
auto proposed_name = opt_proposed_name.value();
if (unavailable_names.count(proposed_name)) {
// The name is already used, either from a GlobalVar, or
// from a previous round of attempted names.
} else if (auto it = new_names.find(proposed_name); it != new_names.end()) {
// The name is not unique within the current attempt. Mark
// the function as nullptr to previous any use of this name
it->second = nullptr;
} else {
// The name is unique so far. Track it for use.
new_names.insert({proposed_name, func});
}
}
}
for (const auto& [name, func] : new_names) {
if (func) {
lifted_names.insert({func, name});
remaining_to_name.erase(func);
}
}
};
// 1. Start with any publicly explosed names from kGlobalSymbol
attempt_name_generation([&](const FunctionNode* func,
const auto&) -> ffi::Optional<ffi::String> {
if (auto it = lifted_with_global_symbol_.find(func); it != lifted_with_global_symbol_.end()) {
return it->second;
} else {
return std::nullopt;
}
});
// 2. Try concatenating the name of the relax variable with the
// name of the function that contains it.
attempt_name_generation([&](const FunctionNode*, const auto& location) -> ffi::String {
std::stringstream stream;
stream << location.front() << "_" << location.back();
return stream.str();
});
// 3. Try concatenating the entire path together. Don't include
// paths of length 2, as they would already be attempted earlier.
attempt_name_generation(
[&](const FunctionNode*, const auto& location) -> ffi::Optional<ffi::String> {
if (location.size() == 2) return std::nullopt;
std::stringstream stream;
bool is_first = true;
for (const auto& loc : location) {
if (is_first) {
is_first = false;
} else {
stream << "_";
}
stream << loc;
}
return ffi::String(stream.str());
});
// 4. Fallback. Count the number of times a relax variable with
// that name was used.
std::unordered_map<ffi::String, int> usage_count;
attempt_name_generation([&](const FunctionNode*, const auto& location) -> ffi::String {
std::stringstream stream;
stream << location.front() << "_" << location.back();
int usage = usage_count[stream.str()]++;
stream << "_" << usage;
return stream.str();
});
TVM_FFI_ICHECK(remaining_to_name.empty())
<< "Fallback failed to make unique names for all lifted lambda functions";
return lifted_names;
}
ffi::Array<ffi::String> name_stack_;
std::unordered_set<ffi::String> previous_global_vars_;
std::unordered_map<ffi::String, ffi::Array<ffi::String>> new_public_names_;
std::unordered_map<const FunctionNode*, ffi::String> lifted_with_global_symbol_;
std::unordered_map<const FunctionNode*, ffi::Array<ffi::String>> lambda_location_;
};
} // namespace
/* The goal of this class is to lift out any nested functions into top-level
* functions.
*
* We will lift a function out into a global which takes the set of the free
* vars and then return the new created function.
*/
class LambdaLifter : public ExprMutator {
public:
explicit LambdaLifter(const IRModule& module)
: ExprMutator(module), mod_(module), lifted_names_(LambdaNameCollector::Collect(module)) {}
using ExprMutator::VisitExpr_;
void VisitBinding_(const VarBindingNode* binding, const FunctionNode* func_node) final {
auto cache = current_lambda_var_;
current_lambda_var_ = binding->var;
auto new_value = VisitExpr(binding->value);
if (!rebind_map_.count(binding->var)) {
ReEmitBinding(binding, new_value);
}
current_lambda_var_ = cache;
}
Expr VisitExpr_(const FunctionNode* func_node) final {
if (!current_lambda_var_) {
// Early bail-out for top-level functions
return ExprMutator::VisitExpr_(func_node);
}
auto func = ffi::GetRef<Function>(func_node);
ffi::String lift_func_name = [&]() {
auto it = lifted_names_.find(func_node);
TVM_FFI_CHECK(it != lifted_names_.end(), InternalError)
<< "Found lambda function during mutation step, "
<< "but it wasn't found during the earlier name-generation step.";
return it->second;
}();
ffi::Array<Var> captured_vars;
bool is_recursive = false;
bool is_closure = false;
for (const auto& var : FreeVars(func)) {
if (current_lambda_var_.same_as(var)) {
is_recursive = true;
} else {
is_closure = true;
captured_vars.push_back(var);
}
}
ffi::Array<Var> typed_captured_vars;
ffi::Map<Var, Expr> rebinding_map;
for (auto free_var : captured_vars) {
Var var = Var(free_var->name, GetType(free_var), free_var->span);
typed_captured_vars.push_back(var);
rebinding_map.Set(free_var, var);
}
tvm::ffi::Array<Var> lifted_func_params =
func_node->params.Map([this](Var var) { return VisitVarDef(var); });
for (const auto& var : typed_captured_vars) {
lifted_func_params.push_back(var);
}
auto gvar_lifted_func = GlobalVar(lift_func_name);
{
auto func_ty = func_node->ty.as_or_throw<FuncType>();
if (is_closure) {
func_ty = FuncType(lifted_func_params.Map(GetType), func_ty->ret, func_ty->purity);
}
UpdateType(gvar_lifted_func, func_ty);
}
Expr body = func_node->body;
// Defining the rewrite rule prior to visiting the body, so that
// recursive closures can be updated.
if (is_recursive && is_closure) {
nested_closure_map_.emplace(current_lambda_var_.value(),
Call(Type::Missing(), gvar_lifted_func,
captured_vars.Map([](Var var) -> Expr { return var; })));
}
if (!is_closure) {
rebind_map_.emplace(current_lambda_var_.value(), gvar_lifted_func);
}
body = this->VisitWithNewScope(body, lifted_func_params);
Type ret_ty = GetType(body);
body = Bind(body, rebinding_map);
Function lifted_func;
if (lifted_func_params.same_as(func_node->params) && body.same_as(func_node->body) &&
ret_ty.same_as(func_node->ret_ty)) {
lifted_func = ffi::GetRef<Function>(func_node);
} else {
lifted_func =
Function(lifted_func_params, body, ret_ty, func_node->is_pure, func_node->attrs);
}
TVM_FFI_ICHECK(lifted_func.defined());
if (is_closure || IsClosure(lifted_func)) {
closures_.insert(gvar_lifted_func);
}
// Add the lifted function to the module.
lifted_func = CopyWithNewVars(lifted_func);
gvar_lifted_func->ty = GetType(lifted_func);
builder_->UpdateFunction(gvar_lifted_func, lifted_func);
Expr callable_value = gvar_lifted_func;
if (is_closure) {
// If we need to allocate a closure,
// we pass the variables in its environment here.
Tuple arg_tuple(captured_vars.Map([](Var var) -> Expr { return var; }));
// Call make_closure intrinsic
callable_value =
Call(Type::Missing(), make_closure_op_, {gvar_lifted_func, arg_tuple}, {}, {});
}
return callable_value;
}
Expr VisitExpr_(const CallNode* call_node) final {
auto call = ffi::GetRef<Call>(call_node);
auto orig_ty = call->ty.as_or_throw<Type>();
if (auto opt_var = call->op.as<Var>()) {
auto var = opt_var.value();
// Call "relax.invoke_closure" to invoke closure
auto bound_value = LookupBinding(var);
if (IsClosure(var) && bound_value.as<CallNode>()) {
// if the original op was pure, we should use invoke_pure_closure
Call orig_call = bound_value.value().as_or_throw<Call>();
bool is_pure = [&]() -> bool {
if (auto op = orig_call->op.as<Op>()) {
static const auto& purity_map = Op::GetAttrMap<bool>("FPurity");
return purity_map.get(op.value(), false);
} else if (const auto* func_ty = orig_call->op->ty.as<FuncTypeNode>()) {
return func_ty->purity;
} else {
TVM_FFI_THROW(InternalError)
<< "Could not determine purity of call to " << orig_call->op
<< ", as it is neither a tvm::Op (type = \"" << orig_call->op->GetTypeKey()
<< "\"), "
<< "nor is is annotated with FuncType (ty = " << orig_call->op->ty << ")";
}
}();
auto prev = call;
call = Call(Type::Missing(), is_pure ? invoke_pure_closure_op_ : invoke_closure_op_,
{var, Tuple(call->args)}, {}, {orig_ty});
}
}
if (auto opt_var = call->op.as<Var>()) {
auto var = opt_var.value();
if (auto it = nested_closure_map_.find(var); it != nested_closure_map_.end()) {
Call nested_call = it->second;
ffi::Array<Expr> new_args = call->args;
for (const auto arg : nested_call->args) {
new_args.push_back(arg);
}
auto prev = call;
call = Call(Type::Missing(), nested_call->op, new_args, call->attrs, call->ty_args);
}
}
return ExprMutator::VisitExpr_(call.get());
}
Expr VisitExpr_(const VarNode* op) override {
auto var = ffi::GetRef<Var>(op);
if (auto it = rebind_map_.find(var); it != rebind_map_.end()) {
return it->second;
}
return ExprMutator::VisitExpr_(op);
}
bool IsClosure(Expr val) {
if (auto opt_var = val.as<Var>()) {
if (closures_.count(opt_var.value())) {
return true;
}
if (auto bound_value = LookupBinding(opt_var.value())) {
val = bound_value.value();
}
}
if (const auto* call_node = val.as<tvm::CallNode>()) {
// recursive call
auto op = call_node->op;
if (auto local_var = op.as<Var>()) {
return IsClosure(local_var.value());
} else if (auto global_var = op.as<GlobalVar>()) {
return IsClosure(global_var.value());
} else {
return make_closure_op_.same_as(op);
}
} else if (const auto* global_var = val.as<GlobalVarNode>()) {
if (closures_.count(ffi::GetRef<GlobalVar>(global_var))) {
return true;
}
IRModule ctx_mod = builder_->GetContextIRModule();
TVM_FFI_ICHECK(ctx_mod->functions.size() > 0);
BaseFunc func = ctx_mod->Lookup(ffi::GetRef<GlobalVar>(global_var));
const auto* func_node = func.as<FunctionNode>();
if (func_node) {
return IsClosure(func_node->body);
} else {
return false;
}
} else if (const auto* func_node = val.as<FunctionNode>()) {
return IsClosure(func_node->body);
} else if (const auto* seq_node = val.as<SeqExprNode>()) {
return IsClosure(seq_node->body);
} else {
return false;
}
}
IRModule Lift() {
auto glob_funcs = mod_->functions;
for (auto [gvar, base_func] : glob_funcs) {
if (auto opt = base_func.as<Function>()) {
// Must visit the function itself, and not just the function
// body, to ensure that EraseToWellDefined recognized symbolic
// variables that are exposed by the function signature.
auto func = VisitExpr(opt.value()).as_or_throw<Function>();
builder_->UpdateFunction(gvar, func);
}
}
return builder_->GetContextIRModule();
}
private:
std::unordered_map<Var, Call> nested_closure_map_;
std::unordered_map<Var, Expr> rebind_map_;
std::unordered_set<ffi::Variant<GlobalVar, Var>, ffi::ObjectPtrHash, ffi::ObjectPtrEqual>
closures_;
ffi::Optional<Var> current_lambda_var_ = std::nullopt;
IRModule mod_;
std::unordered_map<const FunctionNode*, ffi::String> lifted_names_;
/*! \brief Cache ops that would be used later to reduce lookup overhead. */
const Op& make_closure_op_ = Op::Get("relax.make_closure");
const Op& invoke_closure_op_ = Op::Get("relax.invoke_closure");
const Op& invoke_pure_closure_op_ = Op::Get("relax.invoke_pure_closure");
};
namespace transform {
Pass LambdaLift() {
auto pass_func = [=](IRModule mod, PassContext pc) { return relax::LambdaLifter(mod).Lift(); };
return tvm::transform::CreateModulePass(pass_func, 1, "LambdaLift", {});
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def("relax.transform.LambdaLift", LambdaLift);
}
} // namespace transform
} // namespace relax
} // namespace tvm