blob: 39365e84ae153a75a595e80b8f65aaea550bee99 [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/append_loss.cc
* \brief A tool to append the loss function to the backbone function in an IRModule.
*/
#include "utils.h"
#include <tvm/ffi/cast.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/relax/expr.h>
#include <tvm/relax/expr_functor.h>
#include <tvm/relax/transform.h>
#include <unordered_set>
#include "../transform/utils.h"
namespace tvm {
namespace relax {
/*! \brief Append the loss function to the backbone function in an IRModule.*/
class AppendLossMutator : private ExprMutator {
public:
static IRModule Transform(IRModule mod, ffi::String func_name, Function loss_function,
int num_backbone_outputs, ffi::Optional<ffi::String> new_func_name) {
auto* old_func = mod->Lookup(func_name).as<FunctionNode>();
TVM_FFI_ICHECK(old_func) << func_name << "is not a Relax Function";
// functions should be copied to satisfy the well-formed check
Function new_func = CopyWithNewVars(ffi::GetRef<Function>(old_func));
Function new_loss_func = CopyWithNewVars(loss_function);
AppendLossMutator mutator(mod, new_loss_func, num_backbone_outputs);
auto new_func_transformed =
WithAttr(mutator.VisitExpr(new_func).as_or_throw<Function>(), tvm::attr::kGlobalSymbol,
new_func_name.value_or(func_name + "_loss"));
auto new_module = ffi::GetRef<IRModule>(mod.CopyOnWrite());
auto new_var = GlobalVar(new_func_name.value_or(func_name + "_loss"));
new_module->Add(new_var, new_func_transformed);
return new_module;
}
private:
AppendLossMutator(const IRModule& module, const Function& loss_function, int num_backbone_outputs)
: ExprMutator(module),
loss_function_(loss_function),
num_backbone_outputs_(num_backbone_outputs) {}
Expr VisitExpr_(const FunctionNode* func) final {
// Well-formed checks and setting up class members
loss_body_ = loss_function_->body;
CheckLossBody();
BackboneReturnToArr(func->body->body);
CheckAndRemapBackboneReturn();
CheckAndRemapLossParams(loss_function_->params);
ffi::Array<Var> new_params = func->params;
new_params.insert(new_params.end(), loss_function_->params.begin() + num_backbone_outputs_,
loss_function_->params.end());
Expr new_body = this->VisitExpr(func->body);
return Function(new_params, new_body, std::nullopt, func->is_pure, func->attrs);
}
Expr VisitExpr_(const SeqExprNode* seq_expr) final {
TVM_FFI_ICHECK(seq_expr->blocks.size() == 1 &&
seq_expr->blocks[0]->IsInstance<DataflowBlockNode>())
<< "Backbone should have only one DataflowBlock";
auto new_blocks = ffi::Array<BindingBlock>({this->VisitBindingBlock(seq_expr->blocks[0])});
auto ret = ffi::Array<Expr>({loss_body_->body});
ret.insert(ret.end(), backbone_return_arr_.begin() + num_backbone_outputs_,
backbone_return_arr_.end());
return SeqExpr(new_blocks, ret.size() == 1 ? ret[0] : Tuple(ret));
}
BindingBlock VisitBindingBlock_(const DataflowBlockNode* block) final {
builder_->BeginDataflowBlock();
// Emit original bindings.
for (const auto& binding : block->bindings) {
this->VisitBinding(binding);
}
// Emit bindings in the loss function.
for (const Binding& binding : loss_body_->blocks[0]->bindings) {
this->VisitBinding(binding);
}
return builder_->EndBlock();
}
/*!
* \brief Using VisitExpr to remap the defined variable. This is different from the standard
* behaviour of VisitVarDef.
*/
Var VisitVarDef(const Var& var) final { return this->VisitExpr(var).as_or_throw<Var>(); }
/*! \brief Checks the loss function have only one DataflowBlock, and returns a scalar Var. */
void CheckLossBody() {
TVM_FFI_ICHECK(loss_body_->blocks.size() == 1 &&
loss_body_->blocks[0]->IsInstance<DataflowBlockNode>())
<< "The loss function should have only one DataflowBlock";
auto var_node = loss_body_->body.as<VarNode>();
TVM_FFI_ICHECK(var_node && IsScalarTensor(ffi::GetRef<Var>(var_node)))
<< "The loss function must return a scalar(0-dim Tensor) Var";
}
/*!
* \brief Convert the return value of the backbone to ffi::Array<Var>. The backbone should return
* one or a tuple of Vars.
*/
void BackboneReturnToArr(const Expr& backbone_return) {
if (auto* var = backbone_return.as<VarNode>()) {
backbone_return_arr_.push_back(ffi::GetRef<Var>(var));
} else if (auto* tuple = backbone_return.as<TupleNode>()) {
for (auto i : tuple->fields) {
auto var = i.as<VarNode>();
TVM_FFI_ICHECK(var)
<< "The return value of the backbone should be either a Var or a Tuple of Vars";
backbone_return_arr_.push_back(ffi::GetRef<Var>(var));
}
} else {
TVM_FFI_THROW(InternalError)
<< "The return value of the backbone should be either a Var or a Tuple of Vars";
}
}
/*!
* \brief Check the number of elements in loss_func_params is no less than num_backbone_outputs,
* and the elements in backbone_return_arr_ and loss_func_params have matched ty. Also
* sets up var_remap_ from loss parameter Vars to backbone returned Vars.
*/
void CheckAndRemapLossParams(const ffi::Array<Var>& loss_func_params) {
static ffi::StructuralEqual checker;
TVM_FFI_ICHECK(static_cast<int>(loss_func_params.size()) >= num_backbone_outputs_)
<< "The number of parameters of the loss function is " << loss_func_params.size()
<< ", which is less than the given num_backbone_outputs " << num_backbone_outputs_;
for (int i = 0; i < num_backbone_outputs_; ++i) {
Var loss_param = loss_func_params[i];
Var backbone_ret = backbone_return_arr_[i];
auto loss_param_ty = GetType(loss_param);
auto backbone_ret_ty = GetType(backbone_ret);
TVM_FFI_ICHECK(checker(backbone_ret_ty, loss_param_ty))
<< "The type of the " << i
<< "-th return value of backbone function is: " << backbone_ret_ty
<< " while the corresponding type of parameter of loss function is " << loss_param_ty
<< ", which is different.";
this->var_remap_[loss_param] = backbone_ret;
}
}
/*!
* \brief Check the number of elements in backbone_return_arr_ is no less than
* num_backbone_outputs. Then remap Vars in backbone return values that satisfy these conditions
* from Var to DataflowVar:
*
* 1. Is used in prediction_outputs of the backbone function,
* 2. Is not used in other_outputs of the backbone function.
* 3. Does not have PrimType.
*
* Because such Vars are no longer the outputs of the new function. Primitive Vars remain
* ordinary bindings because they may define dependent types in the appended loss function.
*/
void CheckAndRemapBackboneReturn() {
TVM_FFI_ICHECK(static_cast<int>(backbone_return_arr_.size()) >= num_backbone_outputs_)
<< "The number of return values of the backbone function is " << backbone_return_arr_.size()
<< ", which is less than the given num_backbone_outputs " << num_backbone_outputs_;
std::unordered_set<Var, ffi::ObjectPtrHash> other_outputs_var(
backbone_return_arr_.begin() + num_backbone_outputs_, backbone_return_arr_.end());
for (int i = 0; i < num_backbone_outputs_; ++i) {
auto var = backbone_return_arr_[i];
if (other_outputs_var.count(var) == 0 && !var->ty.as<PrimTypeNode>()) {
auto new_var = DataflowVar(var->name, GetType(var), var->span);
this->var_remap_[var] = new_var;
backbone_return_arr_.Set(i, new_var);
}
}
}
/*! \brief The loss function. */
Function loss_function_;
/*! \brief The number of prediction_outputs of the backbone function. */
int num_backbone_outputs_;
/*! \brief The body of the loss function */
SeqExpr loss_body_;
/*! \brief The unpacked return values of the backbone. All return values should be Vars. */
ffi::Array<Var> backbone_return_arr_;
};
namespace transform {
Pass AppendLoss(ffi::String func_name, Function loss_function, int num_backbone_outputs,
ffi::Optional<ffi::String> new_func_name) {
auto pass_func = [=](IRModule mod, PassContext pc) {
return relax::AppendLossMutator::Transform(mod, func_name, loss_function, num_backbone_outputs,
new_func_name);
};
return CreateModulePass(/*pass_function=*/pass_func,
/*opt_level=*/0,
/*pass_name=*/"AppendLoss",
/*required=*/{});
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def("relax.training.AppendLoss", AppendLoss);
}
} // namespace transform
} // namespace relax
} // namespace tvm