blob: e64670b451b716e30181bab7d020944776f8cbba [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 bind_target.cc
* \brief Pass to bind target to primfunc for heterogeneous compilation.
*
* This pass analyzes function call patterns in an IRModule and binds appropriate
* targets (host/device) to each PrimFunc based on where they are called from.
*
* The pass handles the following scenarios:
* 1. Functions called from host code (CPU)
* 2. Functions called from device code (GPU/accelerator)
* 3. Functions called from both host and device
* 4. Externally exposed functions (entry points)
*
* For functions called from both host and device, the pass creates duplicates
* with appropriate targets and updates call sites accordingly.
*/
#include <tvm/ffi/cast.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ir/unique_name_supply.h>
#include <tvm/s_tir/stmt.h>
#include <tvm/s_tir/transform.h>
#include <tvm/tirx/stmt_functor.h>
#include <tvm/tirx/transform.h>
#include <unordered_set>
#include "tvm/ir/attrs.h"
namespace tvm {
namespace tirx {
/*!
* \brief Visitor class to classify function calls as host or device calls.
*
* This visitor traverses the IRModule to identify which functions are called
* from host code vs device code. It tracks GPU scopes (thread binding loops
* and thread extent attributes) to determine the calling context.
*/
class FunctionClassifierVisitor : public StmtExprVisitor {
public:
/*!
* \brief Analyze function call patterns in the IRModule.
* \param mod The IRModule to analyze
* \return A tuple containing:
* - Set of GlobalVarNodes called from host code
* - Set of GlobalVarNodes called from device code
* \note A single function can be called by both host and device contexts.
*/
static std::tuple<std::unordered_set<const GlobalVarNode*>,
std::unordered_set<const GlobalVarNode*>>
GetFunctionCallers(const IRModule& mod) {
FunctionClassifierVisitor visitor;
// Only analyze externally exposed functions as potential callers
// since they represent the entry points where host/device calls originate
for (const auto& [gvar, func] : mod->functions) {
bool is_externally_exposed = func->GetAttr<ffi::String>(tvm::attr::kGlobalSymbol).has_value();
const auto* prim_func = func.as<PrimFuncNode>();
if (is_externally_exposed && prim_func != nullptr) {
visitor.VisitStmt(prim_func->body);
}
}
return std::make_tuple(visitor.host_called_global_vars_, visitor.device_called_global_vars_);
}
private:
using StmtExprVisitor::VisitStmt_;
void VisitExpr_(const CallNode* op) final {
const auto* global_var = op->op.as<GlobalVarNode>();
if (global_var != nullptr) {
// Classify the call based on current scope
if (is_under_gpu_scope_) {
device_called_global_vars_.insert(global_var);
} else {
host_called_global_vars_.insert(global_var);
}
}
StmtExprVisitor::VisitExpr_(op);
}
void VisitStmt_(const ForNode* op) final {
if (op->kind == ForKind::kThreadBinding) {
// Enter GPU scope for thread binding loops
bool last_is_under_gpu_scope = is_under_gpu_scope_;
is_under_gpu_scope_ = true;
StmtExprVisitor::VisitStmt_(op);
is_under_gpu_scope_ = last_is_under_gpu_scope;
} else {
StmtExprVisitor::VisitStmt_(op);
}
}
void VisitStmt_(const AttrStmtNode* op) final {
if (op->attr_key == attr::thread_extent || op->attr_key == s_tir::attr::virtual_thread ||
op->attr_key == attr::kDeviceEntry) {
// Enter GPU scope for thread extent and virtual thread attributes
bool last_is_under_gpu_scope = is_under_gpu_scope_;
is_under_gpu_scope_ = true;
StmtExprVisitor::VisitStmt_(op);
is_under_gpu_scope_ = last_is_under_gpu_scope;
} else {
StmtExprVisitor::VisitStmt_(op);
}
}
private:
/*! \brief Whether the current statement is under a GPU scope */
bool is_under_gpu_scope_ = false;
/*! \brief Set of functions called from host code */
std::unordered_set<const GlobalVarNode*> host_called_global_vars_;
/*! \brief Set of functions called from device code */
std::unordered_set<const GlobalVarNode*> device_called_global_vars_;
};
/*!
* \brief Mutator class to substitute function calls in host contexts.
*
* This mutator replaces calls to functions that have been duplicated for
* host/device contexts. It only performs substitutions when not under
* GPU scope to ensure device calls remain unchanged.
*/
class CallSubstitutor : public StmtExprMutator {
public:
/*!
* \brief Constructor with function replacement mapping.
* \param replacements Map from original GlobalVar to host-specific GlobalVar
*/
explicit CallSubstitutor(const ffi::Map<GlobalVar, GlobalVar>& replacements)
: replacements_(replacements) {}
/*!
* \brief Substitute function calls in a PrimFunc.
* \param func The PrimFunc to process
* \return The modified PrimFunc with updated calls
*/
PrimFunc Substitute(PrimFunc func) {
auto f = func.CopyOnWrite();
auto body = VisitStmt(f->body);
// Only update if the body actually changed
if (body.same_as(func->body)) {
return func;
}
f->body = std::move(body);
return func;
}
private:
using StmtExprMutator::VisitStmt_;
Expr VisitExpr_(const CallNode* op) final {
auto call = StmtExprMutator::VisitExpr_(op).as_or_throw<Call>();
// Only substitute calls when not under GPU scope
if (!is_under_gpu_scope_) {
if (auto old_gvar = call->op.as<GlobalVar>()) {
if (auto new_gvar = replacements_.Get(old_gvar.value())) {
call.CopyOnWrite()->op = new_gvar.value();
}
}
}
return call;
}
Stmt VisitStmt_(const ForNode* op) final {
if (op->kind == ForKind::kThreadBinding) {
// Enter GPU scope for thread binding loops
bool last_is_under_gpu_scope = is_under_gpu_scope_;
is_under_gpu_scope_ = true;
auto stmt = StmtExprMutator::VisitStmt_(op);
is_under_gpu_scope_ = last_is_under_gpu_scope;
return stmt;
} else {
return StmtExprMutator::VisitStmt_(op);
}
}
Stmt VisitStmt_(const AttrStmtNode* op) final {
if (op->attr_key == attr::thread_extent || op->attr_key == s_tir::attr::virtual_thread ||
op->attr_key == attr::kDeviceEntry) {
// Enter GPU scope for thread extent and virtual thread attributes
bool last_is_under_gpu_scope = is_under_gpu_scope_;
is_under_gpu_scope_ = true;
auto stmt = StmtExprMutator::VisitStmt_(op);
is_under_gpu_scope_ = last_is_under_gpu_scope;
return stmt;
} else {
return StmtExprMutator::VisitStmt_(op);
}
}
private:
/*! \brief Whether the current statement is under a GPU scope */
bool is_under_gpu_scope_ = false;
/*! \brief Mapping from original functions to host-specific duplicates */
ffi::Map<GlobalVar, GlobalVar> replacements_;
};
/*!
* \brief Bind appropriate targets to functions in an IRModule.
*
* This function analyzes the call patterns in the module and binds appropriate
* targets to each PrimFunc based on where they are called from. The binding
* follows these rules:
*
* 1. Externally exposed functions (with global symbol) get the full target
* 2. Functions called only from host get the host target
* 3. Functions called only from device get the device target
* 4. Functions called from both contexts get the device target, and a duplicate
* is created with the host target for host callers
*
* \param mod The IRModule to process
* \param target The target to bind (should include both host and device)
* \return The modified IRModule with targets bound to functions
*/
IRModule BindTarget(IRModule mod, const Target& target) {
// Extract host and device targets
auto target_host = target->host.value_or(Target("llvm")).as_or_throw<Target>();
auto target_without_host = target.WithoutHost();
auto mod_copy_on_write = mod.CopyOnWrite();
auto new_mod = ffi::GetRef<IRModule>(mod_copy_on_write);
// Step 1: Analyze function call patterns
auto [host_called_global_vars, device_called_global_vars] =
FunctionClassifierVisitor::GetFunctionCallers(mod);
// Step 2: Bind target to functions with the following rules:
// 1. If the function has a target, and the target has a host, and the function does not have a
// host, then add the host to the function target
// 2. If the function is marked as host function, bind the host target to the function
// 3. If the function is externally exposed (with global symbol), bind the full target
// 4. If the function is not externally exposed:
// 2.1 If the function is called by both host and device, bind the device target to the current
// function and duplicate the function with the host target.
// 2.2 If the function is called by host only, bind the host target to the current function
// 2.3 If the function is called by device only, bind the device target to the current function
// 2.4 If the function is not called by any host or device, skip binding
// Track duplicated functions for call replacement
ffi::Map<GlobalVar, GlobalVar> host_function_replacements;
UniqueNameSupply global_names(new_mod->functions.begin(), new_mod->functions.end(),
[](const auto& kv) { return kv.first->name_hint; });
for (auto [gvar, func] : mod->functions) {
const auto* prim_func_node = func.as<PrimFuncNode>();
if (prim_func_node == nullptr) {
// Skip non-PrimFunc entries
continue;
}
auto prim_func = ffi::GetRef<PrimFunc>(prim_func_node);
bool is_externally_exposed =
prim_func->GetAttr<ffi::String>(tvm::attr::kGlobalSymbol).has_value();
if (auto func_target = func->GetAttr<Target>(tvm::attr::kTarget)) {
// Rule 1: If the function has a target, and the target has a host, and the function does not
// have a host, then add the host to the function target
auto func_target_host = func_target.value()->GetHost();
auto target_host = target->GetHost();
if (target_host && !func_target_host && is_externally_exposed) {
auto new_target = Target::WithHost(func_target.value(), target_host.value());
new_mod->Update(gvar, WithAttr(std::move(prim_func), tvm::attr::kTarget, new_target));
}
continue;
}
if (prim_func->HasNonzeroAttr(tvm::tirx::attr::kIsHostFunc)) {
// Rule 2: If the function is marked as host function, bind the host target to the function
prim_func = WithAttr(std::move(prim_func), tvm::attr::kTarget,
Target::WithHost(target_host, target_host));
new_mod->Update(gvar, WithoutAttr(std::move(prim_func), tvm::tirx::attr::kIsHostFunc));
continue;
}
if (is_externally_exposed) {
// Rule 3: Externally exposed functions get the full target
new_mod->Update(gvar, WithAttr(std::move(prim_func), tvm::attr::kTarget, target));
} else {
const auto* gvar_node = gvar.get();
bool called_by_host = host_called_global_vars.count(gvar_node);
bool called_by_device = device_called_global_vars.count(gvar_node);
if (called_by_host && called_by_device) {
// Rule 4.1: Called by both host and device
// Bind device target to current function
PrimFunc host_func = s_tir::RenewDefs(prim_func);
new_mod->Update(gvar,
WithAttr(std::move(prim_func), tvm::attr::kTarget, target_without_host));
// Create duplicate with host target for host callers
host_func = WithAttr(std::move(host_func), tvm::attr::kTarget, target_host);
ffi::String host_func_name = gvar->name_hint + "_host";
GlobalVar host_gvar = GlobalVar(global_names->FreshName(host_func_name, false));
new_mod->Add(host_gvar, host_func);
host_function_replacements.Set(gvar, host_gvar);
} else if (called_by_host) {
// Rule 4.2: Called by host only
new_mod->Update(gvar, WithAttr(std::move(prim_func), tvm::attr::kTarget, target_host));
} else if (called_by_device) {
// Rule 4.3: Called by device only
new_mod->Update(gvar,
WithAttr(std::move(prim_func), tvm::attr::kTarget, target_without_host));
} else {
// Rule 4.4: Not called by any context
// NOTE: To keep the current behavior, we bind the target to the full target, but it needs
// further check
new_mod->Update(gvar,
WithAttr(std::move(prim_func), tvm::attr::kTarget, target_without_host));
}
}
}
// Step 3: Update call sites in externally exposed functions
if (!host_function_replacements.empty()) {
CallSubstitutor substitutor(host_function_replacements);
for (auto [gvar, func] : mod->functions) {
const auto* prim_func = func.as<PrimFuncNode>();
if (prim_func == nullptr) {
continue;
}
bool is_externally_exposed =
prim_func->GetAttr<ffi::String>(tvm::attr::kGlobalSymbol).has_value();
if (is_externally_exposed) {
// Update calls in externally exposed functions to use host duplicates
PrimFunc new_func = substitutor.Substitute(func.as_or_throw<PrimFunc>());
new_mod->Update(gvar, new_func);
}
}
}
return new_mod;
}
namespace transform {
/*!
* \brief Create a pass that binds targets to functions in an IRModule.
*
* This pass analyzes the call patterns in the module and binds appropriate
* targets (host/device) to each PrimFunc based on where they are called from.
*
* \param target The target to bind (should include both host and device)
* \return A transform pass that performs target binding
*/
transform::Pass BindTarget(Target target) {
auto fpass = [target](IRModule mod, transform::PassContext ctx) {
return tvm::tirx::BindTarget(mod, target);
};
return tirx::transform::CreateModulePass(fpass, 0, "tirx.BindTarget", {});
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def("tirx.transform.BindTarget", BindTarget);
}
} // namespace transform
} // namespace tirx
} // namespace tvm