blob: 16261e83080d0707ccce5c2dc8a984a967f5aa51 [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.
*/
#include <tvm/ffi/cast.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/relax/expr.h>
#include <tvm/relax/expr_functor.h>
#include <tvm/s_tir/meta_schedule/extracted_task.h>
#include <tvm/target/target.h>
#include <tvm/tirx/function.h>
#include <tvm/tirx/stmt_functor.h>
#include "../../s_tir/meta_schedule/module_equality.h"
namespace tvm {
namespace relax {
namespace backend {
using s_tir::meta_schedule::ExtractedTask;
using s_tir::meta_schedule::ModuleEqual;
using s_tir::meta_schedule::ModuleEquality;
using s_tir::meta_schedule::ModuleHash;
/*!
* \brief Extract the Meta-Schedule tuning task from a given IRModule.
* \note
* 1. The task extractor is responsible for task deduplication. The
* deduplication is achieved by comparing structural hashes of PrimFuncs.
* 2. For a PrimFunc, the weight of its corresponding task is the number
* of times it called by op Call-TIR. Say in an IRModule there are three
* PrimFuncs `fn1`, `fn2` and `fn3` sharing the same structural hash.
* Suppose `fn1` is called by 5 Call-TIR ops among all Relax function,
* `fn2` is called by 3 Call-TIR and `fn3` is called by 5 Call-TIR.
* Then we will have a ExtractedTask for all three functions, whose weight
* is 5 + 3 + 2 = 10.
*/
class BlockCounter : public tirx::StmtVisitor {
public:
static size_t GetSBlockCount(const tirx::PrimFunc& func) {
BlockCounter counter;
counter(func->body);
return counter.count;
}
private:
void VisitStmt_(const tirx::SBlockNode* op) final {
++count;
StmtVisitor::VisitStmt_(op);
}
size_t count{0};
};
class TaskExtractor : public ExprVisitor {
public:
static ffi::Array<ExtractedTask> ExtractTask(IRModule mod, Target target,
ffi::String mod_eq_name) {
TaskExtractor extractor(mod, target, mod_eq_name);
// We go through each Relax function in the module.
for (const auto& kv : mod->functions) {
if (const auto* func = kv.second.as<FunctionNode>()) {
extractor(ffi::GetRef<Function>(func));
}
}
ffi::Array<ExtractedTask> tasks;
for (const auto& it : extractor.func2task_) {
tasks.push_back(it.second);
}
return tasks;
}
private:
explicit TaskExtractor(IRModule mod, Target target, ffi::String mod_eq_name)
: mod_(std::move(mod)),
target_(std::move(target)),
mod_eq_(ModuleEquality::Create(mod_eq_name)),
func2task_(/*bucket_count*/ 0, ModuleHash(*mod_eq_), ModuleEqual(*mod_eq_)) {
normalize_mod_func_ = tvm::ffi::Function::GetGlobal("tvm.s_tir.meta_schedule.normalize_mod");
TVM_FFI_ICHECK(normalize_mod_func_.has_value()) << "Normalization function is not found.";
}
void VisitExpr_(const CallNode* call) final {
static const Op& call_tir_op = Op::Get("relax.call_tir");
// TODO(@tvm-team): When we differentiate the call for tirx function and packed function,
// this logic should be changed accordingly.
if (!call->op.same_as(call_tir_op)) {
// Since the Relax function is of A-normal form, the arguments of this call cannot be another
// Calls. And hence we do not need to recurse into this Call.
return;
}
const GlobalVar& global_var = call->args[0].as_or_throw<GlobalVar>();
const tirx::PrimFunc& func = mod_->Lookup(global_var).as_or_throw<tirx::PrimFunc>();
IRModule mod = (*normalize_mod_func_)(func).cast<IRModule>();
size_t weight = 1;
auto it = func2task_.find(mod);
if (it != func2task_.end()) {
it->second->weight += 1;
const tirx::PrimFunc& alt_func = it->first->Lookup("main").as_or_throw<tirx::PrimFunc>();
// When anchor-block based equality is used, tuning tasks "nn_conv2d_add_nn_relu" and
// "nn_conv2d_add_add_nn_relu", for example, can be identified as equal. Thus, one of them
// will be selected to tune by the code below.
//
// To make sure that we tune "nn_conv2d_add_nn_relu" and not "nn_conv2d_add_add_nn_relu", we
// count the PrinFunc number of blocks and leave only the function with the smallest number of
// blocks. This way, "nn_conv2d_add_nn_relu" will have a smaller number of blocks than
// "nn_conv2d_add_add_nn_relu" and will be selected to tune.
if (BlockCounter::GetSBlockCount(func) < BlockCounter::GetSBlockCount(alt_func)) {
weight += it->second->weight;
func2task_.erase(it->first);
}
}
ExtractedTask task(/*task_name=*/global_var->name_hint, //
/*mod=*/mod, //
/*target=*/target_, //
/*dispatched=*/{mod}, //
/*weight=*/weight);
func2task_.emplace(mod, task);
}
IRModule mod_;
Target target_;
std::unique_ptr<ModuleEquality> mod_eq_;
std::unordered_map<IRModule, ExtractedTask, ModuleHash, ModuleEqual> func2task_;
std::optional<tvm::ffi::Function> normalize_mod_func_;
};
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def("relax.backend.MetaScheduleExtractTask", [](IRModule mod, Target target,
ffi::String mod_eq_name) {
return TaskExtractor::ExtractTask(std::move(mod), std::move(target), std::move(mod_eq_name));
});
}
} // namespace backend
} // namespace relax
} // namespace tvm