blob: b7cbcaa9379fecf828fcd5ea360171d7d1dcfe47 [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/dataflow_inplace.cc
* \brief Pass that converts eligible operator calls in dataflow blocks
* into in-place versions.
*/
#include <tvm/ffi/cast.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ir/transform.h>
#include <tvm/relax/analysis.h>
#include <tvm/relax/attrs/op.h>
#include <tvm/relax/expr.h>
#include <tvm/relax/expr_functor.h>
#include <tvm/relax/transform.h>
#include <tvm/relax/utils.h>
#include <tvm/tirx/stmt_functor.h>
#include "utils.h"
namespace tvm {
namespace relax {
// Ops that may return a tensor sharing storage with the first argument.
// These ops has been verified to share storage with the first argument in
// tests/python/relax/test_dataflow_inplace.py.
bool IsViewMemoryOp(const OpNode* op_node) {
// TODO: Consider to add more ops that may return a tensor sharing storage with
// the first argument in the future.
static const std::unordered_set<std::string> kViewOps = {
"relax.expand_dims", "relax.squeeze",
"relax.reshape", "relax.permute_dims",
"relax.flatten", "relax.nn.batch_flatten",
"relax.memory.view", "relax.memory.ensure_zero_offset",
};
return kViewOps.count(op_node->name);
}
// Look up alias ids for a call argument (only Var args are expected in dataflow blocks).
std::unordered_set<int> GetVarAliasSetFromExpr(
const Expr& arg, const std::unordered_map<Var, std::unordered_set<int>>& alias_sets) {
if (auto* var_node = arg.as<VarNode>()) {
Var var = ffi::GetRef<Var>(var_node);
if (!alias_sets.count(var)) {
return {-1};
}
return alias_sets.at(var);
}
return {-1};
}
// In-place on arg `candidate` is invalid if another distinct operand may alias the same
// storage (e.g. two expand_dims views of x bound to different vars). Reject on any shared
// alias id; -1 in the other operand's set does not skip checking other ids. Same var twice
// (e.g. add(z, z)) is allowed.
bool InplaceArgDisjointFromOtherCallArgs(
const CallNode* call_node, int candidate,
const std::unordered_map<Var, std::unordered_set<int>>& alias_sets) {
const auto* cand_var_node = call_node->args[candidate].as<VarNode>();
if (!cand_var_node) {
return false;
}
auto cand_set = GetVarAliasSetFromExpr(call_node->args[candidate], alias_sets);
if (cand_set.count(-1)) {
return false;
}
for (size_t j = 0; j < call_node->args.size(); j++) {
if (static_cast<int>(j) == candidate) {
continue;
}
const Expr& other_arg = call_node->args[j];
if (other_arg.same_as(call_node->args[candidate])) {
continue;
}
auto other_set = GetVarAliasSetFromExpr(other_arg, alias_sets);
for (int alias_idx : other_set) {
if (cand_set.count(alias_idx)) {
return false;
}
}
}
return true;
}
// Perform liveness analysis on a dataflow block, returning a map of vars to
// pairs of indices (the liveness interval, from the starting index to the end index).
// A starting index of -1 means the var is defined before the block starts and an end index
// of block->bindings.size() (one past the last index) means it is live after the block ends.
std::unordered_map<Var, std::pair<int, int>> AnalyzeLiveness(const DataflowBlock& block) {
std::unordered_map<Var, std::pair<int, int>> ret;
for (int i = block->bindings.size() - 1; i >= 0; i--) {
Binding b = block->bindings[i];
Var defined_var = b->var;
Expr value = GetBoundValue(b);
ffi::Array<Var> used_vars;
// for a function literal, we consider only the free vars
// (those captured from the outer scope)
if (value.as<FunctionNode>()) {
used_vars = FreeVars(value);
} else if (value.as<TupleGetItemNode>()) {
// Special case: we do not consider a tuple index to be a "use."
// This is a bit of a hack but allows us to do operations that
// create tuples to be done in-place (otherwise, any index of the tuple
// would be considered a use and so the tuple would be live later).
// Hence we keep the array empty.
} else {
used_vars = AllVars(value);
}
for (auto var : used_vars) {
int range_end = i;
// if the var is not a dataflow var, then it is live
// after the block (we are not checking later blocks)
if (!var.as<DataflowVarNode>()) {
range_end = block->bindings.size();
}
if (!ret.count(var)) {
ret[var] = {-1, range_end};
}
}
if (!ret.count(defined_var)) {
// if it's an output, then it lives past the end of the block
if (!defined_var.as<DataflowVarNode>()) {
ret[defined_var] = {i, block->bindings.size()};
} else {
// otherwise, it's live only here
ret[defined_var] = {i, i};
}
} else {
// this means the var is used later but we encountered its definition now
auto last_range = ret[defined_var];
TVM_FFI_ICHECK_EQ(last_range.first, -1);
std::pair<int, int> new_range = {i, last_range.second};
ret[defined_var] = new_range;
}
}
return ret;
}
class AliasAnalyzer {
public:
AliasAnalyzer() : alias_map_(), tuple_map_(), mem_idx_(0) {}
// The analysis returns a map of vars to memory locations that it *could* map to
// (any unique allocation = one memory location), plus a map of memory locations
// that correspond to tuples (this maps to sets of memory locations for each tuple element).
// Note: inputs are values that should be assumed not to be aliased and are therefore
// (in the case of in-place ops) safe to overwrite. This may not be true of function args.
std::pair<std::unordered_map<Var, std::unordered_set<int>>,
std::unordered_map<int, std::vector<std::unordered_set<int>>>>
Analyze(const DataflowBlock& block, const ffi::Array<Var>& inputs) {
for (auto input : inputs) {
int curr_idx = get_fresh_idx();
alias_map_[input] = {curr_idx};
if (auto* tup_info = GetTypeAs<TupleTypeNode>(input)) {
InsertFreshTuple(curr_idx, tup_info);
}
}
for (const Binding& binding : block->bindings) {
Var current_var = binding->var;
Expr value = GetBoundValue(binding);
alias_map_[current_var] = GetAliasSet(value, current_var);
}
return {alias_map_, tuple_map_};
}
private:
int get_fresh_idx() {
int ret = mem_idx_;
mem_idx_++;
return ret;
}
// Fresh tuple = each element is assumed to be a unique allocation
void InsertFreshTuple(int tup_idx, const TupleTypeNode* tup_info) {
std::vector<std::unordered_set<int>> tuple_set;
for (int i = 0; i < static_cast<int>(tup_info->fields.size()); i++) {
int curr_field = get_fresh_idx();
tuple_set.push_back({curr_field});
if (auto* nested_tup_info = tup_info->fields[i].as<TupleTypeNode>()) {
InsertFreshTuple(curr_field, nested_tup_info);
}
}
tuple_map_[tup_idx] = tuple_set;
}
// given a tuple index, add the given memory location indices to each component's
// alias set
void UpdateTupleComponents(int tup_idx, const std::unordered_set<int>& insert_idxs) {
if (tuple_map_.count(tup_idx)) {
auto tuple_comps = tuple_map_[tup_idx];
for (size_t i = 0; i < tuple_comps.size(); i++) {
auto comp_set = tuple_comps[i];
// if a member is a tuple, update its components as well
for (int member : comp_set) {
if (tuple_map_.count(member)) {
UpdateTupleComponents(member, insert_idxs);
}
}
// update after iterating to avoid iterating over the inserted elements
tuple_map_[tup_idx][i].insert(insert_idxs.begin(), insert_idxs.end());
}
}
}
// capture the given index and also its tuple components (including recursively)
// if they exist
void AddCapturedIndices(std::unordered_set<int>* captured_set, int idx) {
captured_set->insert(idx);
if (tuple_map_.count(idx)) {
for (auto comp_set : tuple_map_[idx]) {
for (auto tup_comp_idx : comp_set) {
AddCapturedIndices(captured_set, tup_comp_idx);
}
}
}
}
// Conservative extremely pessimistic assumption:
// assume that the result of a non-op call can be aliased to any argument
// or that it could be a newly allocated value.
// For tuples, assume all members are aliased. Yeah, it's bad.
// (Skip first arg is for handling call_pure_packed, where the first arg is an ExternFunc that we
// should ignore)
std::unordered_set<int> HandleMysteryCall(const CallNode* call_node, const Var& bound_var,
bool skip_first_arg = false) {
// the result may or may not be newly allocated
std::unordered_set<int> ret;
int res_idx = get_fresh_idx();
// the result may be a tuple
if (auto* tup_info_node = GetTypeAs<TupleTypeNode>(bound_var)) {
InsertFreshTuple(res_idx, tup_info_node);
}
AddCapturedIndices(&ret, res_idx);
for (size_t i = (skip_first_arg) ? 1 : 0; i < call_node->args.size(); i++) {
auto arg = call_node->args[i];
auto arg_alias_set = GetAliasSet(arg, bound_var);
for (int alias_idx : arg_alias_set) {
AddCapturedIndices(&ret, alias_idx);
}
}
// if the result is a tuple, the components can also potentially be aliased to any arg
// or, in fact, to each other
UpdateTupleComponents(res_idx, ret);
return ret;
}
// given the expression value, return the set of memory locations corresponding to it
// (the var the expression is being bound to is needed for type)
std::unordered_set<int> GetAliasSet(const Expr& value, const Var& bound_var) {
std::unordered_set<int> ret;
// cases for value:
// constant: it's a fresh index
// var: look up in alias map (-1 if not present)
// op call: assume it's fresh (may need to make list of exceptions)
// tuple: fresh entry in tuple index, recurse to determine indices for values
// function/packed call: chaos reigns, alias with any other argument
// (if tuple is passed, assume also aliased with all members of the tuple)
// tuple index: -1 if tuple is not in tuple map, otherwise look up corresponding entry
// function constant: give them a fresh index (TODO: we can handle in more detail if this is a
// case we need to support) prim value: fresh index if node: should not happen inside dataflow
// block
if (value.as<ConstantNode>() || value.as<FunctionNode>()) {
// TODO(@slyubomirsky): We will probably want special handling for closures
ret.insert(get_fresh_idx());
} else if (auto* target_var_node = value.as<VarNode>()) {
auto target_var = ffi::GetRef<Var>(target_var_node);
if (alias_map_.count(target_var)) {
ret.insert(alias_map_[target_var].begin(), alias_map_[target_var].end());
} else {
ret.insert(-1);
}
} else if (value.as<PrimExpr>()) {
ret.insert(get_fresh_idx());
} else if (auto* target_tuple = value.as<TupleNode>()) {
// fresh idx but we update the tuple map
int tup_idx = get_fresh_idx();
ret.insert(tup_idx);
std::vector<std::unordered_set<int>> new_tuple_map;
for (auto field : target_tuple->fields) {
new_tuple_map.push_back(GetAliasSet(field, bound_var));
}
tuple_map_[tup_idx] = new_tuple_map;
} else if (auto* target_tgi = value.as<TupleGetItemNode>()) {
std::unordered_set<int> tuple_set = GetAliasSet(target_tgi->tuple, bound_var);
// if -1 is a member of the tuple set, then we have to assume the result is -1
if (tuple_set.count(-1)) {
ret.insert(-1);
} else {
// otherwise, consider all members that are tuples of appropriate size and index into them
// (this is safe because the type system will ensure we're not indexing into a tuple
// of the wrong size)
for (int member : tuple_set) {
if (tuple_map_.count(member) &&
static_cast<int>(tuple_map_[member].size()) > target_tgi->index) {
auto member_set = tuple_map_[member][target_tgi->index];
ret.insert(member_set.begin(), member_set.end());
}
}
}
} else if (auto* call_node = value.as<CallNode>()) {
if (auto* op_node = call_node->op.as<OpNode>()) {
// call_pure_packed: treat as non-op call
if (op_node->name == "relax.call_pure_packed") {
return HandleMysteryCall(call_node, bound_var, true);
} else if (op_node->name == "relax.call_tir") {
// call_tir: can potentially return a tuple
if (auto* tuple_ty = call_node->ty_args[0].as<TupleTypeNode>()) {
int tup_idx = get_fresh_idx();
ret.insert(tup_idx);
InsertFreshTuple(tup_idx, tuple_ty);
} else {
ret.insert(get_fresh_idx());
}
} else if (IsViewMemoryOp(op_node) && !call_node->args.empty()) {
// View-like ops may share storage with their input (and with other views of it).
return GetAliasSet(call_node->args[0], bound_var);
} else {
// We are assuming most op calls return fresh values.
// We may have to track more exceptions
// If the returned value is a tuple, we'll assume it's a fresh tuple
// (there may be exceptions to this too)
if (auto* tup_info = GetTypeAs<TupleTypeNode>(bound_var)) {
int tup_idx = get_fresh_idx();
ret.insert(tup_idx);
InsertFreshTuple(tup_idx, tup_info);
return ret;
}
ret.insert(get_fresh_idx());
}
} else {
// assume any non-op call can be extremely dangerous and do anything
return HandleMysteryCall(call_node, bound_var);
}
}
return ret;
}
std::unordered_map<Var, std::unordered_set<int>> alias_map_;
std::unordered_map<int, std::vector<std::unordered_set<int>>> tuple_map_;
int mem_idx_;
};
// given a shape, return the number of elements corresponding to it (product of elements)
PrimExpr NumElements(const ShapeExpr& shape) {
PrimExpr ret = IntImm::Int64(1);
for (auto dim : shape->values) {
ret *= dim;
}
return ret;
}
// Given the type of the result, return any type nested in it
// that is eleigible to be used for in-place computations (tensors are eligible
// only if all their dimensions are integer constants, tuples are eligible if
// all members are eligible though we can consider only individual members separately)
std::unordered_set<Type, ffi::ObjectPtrHash, ffi::ObjectPtrEqual> GatherCandidateType(
const Type& result_ty) {
if (auto* tensor_info = result_ty.as<TensorTypeNode>()) {
// don't consider void dtype (don't know the size at compile time)
if (tensor_info->IsUnknownDtype()) {
return {};
}
// don't consider cases where we don't know the shape at compile time
// (we will use the analyzer to do best-effort analysis where there are vars)
if (tensor_info->shape.as<ShapeExprNode>()) {
return {ffi::GetRef<TensorType>(tensor_info)};
} else {
return {};
}
} else if (auto* tuple_info = result_ty.as<TupleTypeNode>()) {
// we can see if the whole tuple matches or go for any of the components
std::unordered_set<Type, ffi::ObjectPtrHash, ffi::ObjectPtrEqual> ret;
for (auto field : tuple_info->fields) {
auto field_candidates = GatherCandidateType(field);
ret.insert(field_candidates.begin(), field_candidates.end());
}
// at least one field should be eligible to be done in-place
if (!ret.empty()) {
ret.insert(ffi::GetRef<Type>(tuple_info));
}
return ret;
} else {
// don't consider any other types
return {};
}
}
// Given the two type, return a pair of bools where the first element is true if
// the two type have the same number of elements and dtype and the second element is true
// if the shapes match _exactly_. Performs this check recursively and ensures the
// stated condition is true for all tensor members of the type (return false
// if a single pair of corresponding tensors does not meet the condition).
std::pair<bool, bool> SizeMatches(const Type& target_info, const Type& arg_info,
const BlockBuilder& ctx) {
if (target_info.as<TensorTypeNode>() && arg_info.as<TensorTypeNode>()) {
auto target_tensor = target_info.as_or_throw<TensorType>();
auto arg_tensor = arg_info.as_or_throw<TensorType>();
if (target_tensor->shape.has_value() && target_tensor->shape.as<ShapeExprNode>() &&
arg_tensor->shape.has_value() && arg_tensor->shape.as<ShapeExprNode>()) {
if (target_tensor->dtype != arg_tensor->dtype) {
return {false, false};
}
auto target_shape = target_tensor->shape.value().as_or_throw<ShapeExpr>();
auto arg_shape = arg_tensor->shape.value().as_or_throw<ShapeExpr>();
PrimExpr target_size = NumElements(target_shape);
PrimExpr arg_size = NumElements(arg_shape);
if (!ctx->GetAnalyzer()->CanProve(arg_size >= target_size)) {
return {false, false};
}
// exact match: number of dims and each dim matches
if (target_shape->values.size() == arg_shape->values.size()) {
for (size_t i = 0; i < target_shape->values.size(); i++) {
if (!ctx->GetAnalyzer()->CanProveEqual(target_shape->values[i], arg_shape->values[i])) {
return {true, false};
}
}
return {true, true};
}
return {true, false};
} else {
return {false, false};
}
} else if (target_info.as<TupleTypeNode>() && arg_info.as<TupleTypeNode>()) {
auto target_tup = target_info.as_or_throw<TupleType>();
auto arg_tup = arg_info.as_or_throw<TupleType>();
if (target_tup->fields.size() != arg_tup->fields.size()) {
return {false, false};
}
bool all_exact = true;
for (size_t i = 0; i < target_tup->fields.size(); i++) {
// if members aren't either tuples or tensors, simply skip them,
// since they don't matter for in-place computations
if (!(target_tup->fields[i].as<TensorTypeNode>() ||
target_tup->fields[i].as<TupleTypeNode>()) &&
!(arg_tup->fields[i].as<TensorTypeNode>() || arg_tup->fields[i].as<TupleTypeNode>())) {
continue;
}
auto [field_size_match, field_exact_match] =
SizeMatches(target_tup->fields[i], arg_tup->fields[i], ctx);
if (!field_size_match) {
return {false, false};
}
all_exact = all_exact && field_exact_match;
}
return {true, all_exact};
} else {
return {false, false};
}
}
// Given an alias index, check if it's a tuple and gather the sets of aliases for the tuple
// members if so (apply recursively if any of those members are tuples).
// Return false if the alias set contains -1, meaning a reference to an unknown or
// possibly dangerous value (no checking we can do for that).
bool GatherSetsToCheckForLiveness(
const std::unordered_map<Var, std::unordered_set<int>>& alias_sets,
const std::unordered_map<int, std::vector<std::unordered_set<int>>>& tuple_map,
std::vector<std::unordered_set<int>>* sets_to_check, int alias_idx) {
if (tuple_map.count(alias_idx)) {
for (auto member_set : tuple_map.at(alias_idx)) {
// contains -1 -> unknown and dangerous, we can short-circuit
if (member_set.count(-1)) {
return false;
}
sets_to_check->push_back(member_set);
// if a member can be a tuple, check it recursively
for (int member : member_set) {
if (tuple_map.count(member)) {
if (!GatherSetsToCheckForLiveness(alias_sets, tuple_map, sets_to_check, member)) {
return false;
}
}
}
}
}
return true;
}
// Check that the target is not live past the index and that no alias of it is live past the
// binding index (if the target is a tuple, check the conditions recursively for the members)
bool InplaceConditionsMet(
const std::unordered_map<Var, std::pair<int, int>>& live_ranges,
const std::unordered_map<Var, std::unordered_set<int>>& alias_sets,
const std::unordered_map<int, std::vector<std::unordered_set<int>>>& tuple_map,
const std::unordered_set<Var>& currently_live, const Expr& target, int binding_idx) {
if (auto* var_node = target.as<VarNode>()) {
auto current_var = ffi::GetRef<Var>(var_node);
// if the var is live past this point, we can't use it for in-place computations anyway
if (live_ranges.count(current_var)) {
auto live_range = live_ranges.at(current_var);
if (live_range.second > binding_idx) {
return false;
}
}
// no entry for the current var -> it must be something external and we have to assume the worst
if (!alias_sets.count(current_var)) {
return false;
}
auto alias_set = alias_sets.at(current_var);
// -1 -> an external value and we must assume the worst
if (alias_set.count(-1)) {
return false;
}
std::vector<std::unordered_set<int>> sets_to_check = {alias_set};
std::unordered_set<int> indices_checked;
// If a possible alias is a tuple, we will also check for aliases of the members
// (possibly recursively)
for (int alias_idx : alias_set) {
if (!GatherSetsToCheckForLiveness(alias_sets, tuple_map, &sets_to_check, alias_idx)) {
return false;
}
}
for (Var other_var : currently_live) {
if (other_var.same_as(target)) {
continue;
}
// not represented = spooky unknown value that should be modeled by -1
if (!alias_sets.count(other_var) || !live_ranges.count(other_var)) {
continue;
}
// var is not live past this point => don't need to worry
if (live_ranges.at(other_var).second <= binding_idx) {
continue;
}
auto other_alias_set = alias_sets.at(other_var);
for (int alias_idx : other_alias_set) {
for (auto check_set : sets_to_check) {
if (check_set.count(alias_idx)) {
return false;
}
}
}
}
return true;
} else if (auto* tup_node = target.as<TupleNode>()) {
for (auto field : tup_node->fields) {
if (!InplaceConditionsMet(live_ranges, alias_sets, tuple_map, currently_live, field,
binding_idx)) {
return false;
}
}
return true;
} else {
return true;
}
}
// this is obviously not a complete list
static std::unordered_set<std::string> SUPPORTED_OPS = {"relax.add", "relax.subtract",
"relax.multiply", "relax.divide",
"relax.nn.silu", "relax.nn.relu"};
bool OpSupportsInplace(const Op& op) { return SUPPORTED_OPS.count(op->name); }
/*! \brief Corresponds to a binding where at least one argument meets the conditions to be
* made in-place. Contains the binding index and indices of the applicable arguments
*/
class InplaceOpportunityNode : public ffi::Object {
public:
int64_t binding_idx;
ffi::Array<int64_t> arg_idxs;
static void RegisterReflection() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<InplaceOpportunityNode>()
.def_ro("binding_idx", &InplaceOpportunityNode::binding_idx)
.def_ro("arg_idxs", &InplaceOpportunityNode::arg_idxs);
}
TVM_FFI_DECLARE_OBJECT_INFO("relax.transform.InplaceOpportunity", InplaceOpportunityNode,
ffi::Object);
};
TVM_FFI_STATIC_INIT_BLOCK() { InplaceOpportunityNode::RegisterReflection(); }
class InplaceOpportunity : public ffi::ObjectRef {
public:
TVM_DLL InplaceOpportunity(int64_t binding_idx, const ffi::Array<int64_t>& arg_idxs) {
auto node = ffi::make_object<InplaceOpportunityNode>();
node->binding_idx = binding_idx;
node->arg_idxs = arg_idxs;
data_ = std::move(node);
}
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(InplaceOpportunity, ffi::ObjectRef,
InplaceOpportunityNode);
};
// Check for in-place eligibility:
// 1. see if there's an arg big enough to hold the result
// 2. see if the arg is live past the call
// 3. see if the arg has an alias that's live past the call
// If the conditions are met, record the index of that binding.
// Returns two lists of lists:
// 1. A list of bindings where at least one argument meets the in-place conditions and the *size*
// matches the size of the result.
// 2. A list of bindings where at least one argument meets the in-place conditions
// and *exactly* matches the shape of the result.
// For both lists, each element is a list of ints of the following format:
// The first element is the index of the *binding* in the block.
// All remaining elements are the indices of *eligible arguments* in that call.
std::pair<std::vector<InplaceOpportunity>, std::vector<InplaceOpportunity>>
FindInplaceOpportunities(const DataflowBlock& block, const ffi::Array<Var>& inputs,
const BlockBuilder& ctx) {
auto live_ranges = AnalyzeLiveness(block);
AliasAnalyzer analyzer;
auto alias_info = analyzer.Analyze(block, inputs);
auto alias_sets = alias_info.first;
auto tuple_map = alias_info.second;
std::vector<InplaceOpportunity> size_match_list;
std::vector<InplaceOpportunity> exact_match_list;
// sort the live ranges by starting index
std::vector<Var> live_order;
for (auto kv : live_ranges) {
live_order.push_back(kv.first);
}
std::sort(live_order.begin(), live_order.end(),
[&live_ranges](const Var& var1, const Var& var2) -> bool {
return live_ranges[var1].first < live_ranges[var2].first;
});
std::unordered_set<Var> currently_live;
int last_live = 0;
for (size_t i = 0; i < block->bindings.size(); i++) {
// include all vars that are currently live
for (int j = last_live; j < static_cast<int>(live_order.size()); j++) {
auto live_var = live_order[j];
auto live_range = live_ranges[live_var];
if (live_range.first > static_cast<int>(i)) {
break;
}
currently_live.insert(live_var);
last_live++;
}
// remove vars whose range has come to an end
// (keep a separate set to avoid changing the set while iterating on it)
std::unordered_set<Var> remove;
for (auto var : currently_live) {
auto live_range = live_ranges[var];
if (live_range.second < static_cast<int>(i)) {
remove.insert(var);
}
}
for (auto var : remove) {
currently_live.erase(var);
}
// if we reach a binding check the conditions
Binding b = block->bindings[i];
Var defined_var = b->var;
Expr value = GetBoundValue(b);
if (auto* call_node = value.as<CallNode>()) {
if (auto* op_node = call_node->op.as<OpNode>()) {
if (!OpSupportsInplace(ffi::GetRef<Op>(op_node))) {
continue;
}
std::unordered_set<int> candidates;
std::unordered_set<int> exact_match_candidates;
auto target_ty = GatherCandidateType(GetType(defined_var));
// can't be done in-place, ignore
if (target_ty.empty()) {
continue;
}
// Check that at least one argument matches size with the result
for (size_t j = 0; j < call_node->args.size(); j++) {
auto arg = call_node->args[j];
for (auto target : target_ty) {
auto [matches_size, matches_exactly] = SizeMatches(target, GetType(arg), ctx);
if (matches_size) {
candidates.insert(static_cast<int>(j));
if (matches_exactly) {
exact_match_candidates.insert(static_cast<int>(j));
}
}
}
}
if (candidates.empty()) {
continue;
}
// Make sure at least one candidate is not live past this point and does not have an alias
// live past this point
std::unordered_set<int> remove_candidates;
for (auto candidate : candidates) {
if (!InplaceConditionsMet(live_ranges, alias_sets, tuple_map, currently_live,
call_node->args[candidate], i) ||
!InplaceArgDisjointFromOtherCallArgs(call_node, candidate, alias_sets)) {
remove_candidates.insert(candidate);
}
}
// (remove now to avoid modifying the list as we iterate on it)
for (auto candidate : remove_candidates) {
candidates.erase(candidate);
}
// if we have a candidate, then this can be made in-place. Report the appropriate candidates
if (candidates.empty()) {
continue;
}
// produce a list of candidates for this index
ffi::Array<int64_t> size_candidate_list;
for (auto candidate : candidates) {
size_candidate_list.push_back(static_cast<int64_t>(candidate));
}
size_match_list.push_back(InplaceOpportunity(static_cast<int64_t>(i), size_candidate_list));
// also gather up the exact match candidates if there are any
ffi::Array<int64_t> exact_candidate_list;
for (auto candidate : candidates) {
if (!exact_match_candidates.count(candidate)) {
continue;
}
exact_candidate_list.push_back(static_cast<int64_t>(candidate));
}
if (exact_candidate_list.empty()) {
continue;
}
exact_match_list.push_back(
InplaceOpportunity(static_cast<int64_t>(i), exact_candidate_list));
}
}
}
return {size_match_list, exact_match_list};
}
// Replace buffers in a PrimFunc according to the mapping.
tirx::Stmt RemapBuffers(const tirx::Stmt& stmt,
const ffi::Map<tirx::BufferVar, tirx::BufferVar>& buffer_map) {
class BufferMapper : public tirx::StmtExprMutator {
public:
explicit BufferMapper(const ffi::Map<tirx::BufferVar, tirx::BufferVar>& buffer_map)
: buffer_map_(buffer_map) {}
tirx::Stmt Remap(const tirx::Stmt& stmt) { return VisitStmt(stmt); }
Expr VisitExpr_(const tirx::BufferLoadNode* op) final {
auto node = tirx::StmtExprMutator::VisitExpr_(op).as_or_throw<tirx::BufferLoad>();
auto* node_cow = node.CopyOnWrite();
node_cow->buffer = AttemptRemap(node->buffer);
return node;
}
tirx::Stmt VisitStmt_(const tirx::BufferStoreNode* op) final {
auto node = tirx::StmtExprMutator::VisitStmt_(op).as_or_throw<tirx::BufferStore>();
auto* node_cow = node.CopyOnWrite();
node_cow->buffer = AttemptRemap(node->buffer);
return node;
}
tirx::Stmt VisitStmt_(const tirx::DeclBufferNode* op) final {
auto node = tirx::StmtExprMutator::VisitStmt_(op).as_or_throw<tirx::DeclBuffer>();
auto* node_cow = node.CopyOnWrite();
node_cow->buffer = AttemptRemap(node->buffer);
return node;
}
tirx::Stmt VisitStmt_(const tirx::AllocBufferNode* op) final {
auto node = tirx::StmtExprMutator::VisitStmt_(op).as_or_throw<tirx::AllocBuffer>();
auto* node_cow = node.CopyOnWrite();
node_cow->buffer = AttemptRemap(node->buffer);
return node;
}
tirx::Stmt VisitStmt_(const tirx::SBlockNode* op) final {
auto node = tirx::StmtExprMutator::VisitStmt_(op).as_or_throw<tirx::SBlock>();
auto* node_cow = node.CopyOnWrite();
// need the lambdas because class methods are not first-class (how ironic)
node_cow->alloc_buffers =
node->alloc_buffers.Map([this](const tirx::BufferVar& b) { return AttemptRemap(b); });
node_cow->reads =
node->reads.Map([this](const tirx::BufferRegion& br) { return VisitBufferRegion(br); });
node_cow->writes =
node->writes.Map([this](const tirx::BufferRegion& br) { return VisitBufferRegion(br); });
node_cow->match_buffers = node->match_buffers.Map(
[this](const tirx::MatchBufferRegion& mbr) { return VisitMatchBufferRegion(mbr); });
return node;
}
private:
tirx::BufferVar AttemptRemap(const tirx::BufferVar& buffer) {
if (buffer_map_.count(buffer)) {
return buffer_map_.at(buffer);
}
return buffer;
}
tirx::BufferRegion VisitBufferRegion(tirx::BufferRegion region) {
auto* region_cow = region.CopyOnWrite();
region_cow->buffer = AttemptRemap(region_cow->buffer);
return region;
}
tirx::MatchBufferRegion VisitMatchBufferRegion(tirx::MatchBufferRegion region) {
auto* region_cow = region.CopyOnWrite();
region_cow->buffer = AttemptRemap(region_cow->buffer);
return region;
}
const ffi::Map<tirx::BufferVar, tirx::BufferVar>& buffer_map_;
};
BufferMapper mapper(buffer_map);
auto ret = mapper.Remap(stmt);
return ret;
}
class ModuleInplaceTransformer : public ExprMutator {
public:
explicit ModuleInplaceTransformer(const IRModule& mod) : mod_(mod) {
builder_ = BlockBuilder::Create(mod);
}
IRModule Transform() {
// visit every Relax function in the module
for (auto kv : mod_->functions) {
if (auto* func_node = kv.second.as<FunctionNode>()) {
auto gv = kv.first;
auto func_params = func_node->params;
auto function = VisitExpr(ffi::GetRef<Function>(func_node)).as_or_throw<Function>();
builder_->UpdateFunction(gv, function);
}
}
auto ret = builder_->GetContextIRModule();
// clean up to avoid polluting the IRModule
for (auto gv : legalizers_added) {
ret->Remove(gv);
}
return ret;
}
Expr VisitExpr_(const FunctionNode* op) override {
auto old_func_params = func_params;
func_params = op->params;
auto ret = ExprMutator::VisitExpr_(op);
func_params = old_func_params;
return ret;
}
// the only case we will override: we will visit all binding blocks
// and replace any valid calls in them
BindingBlock VisitBindingBlock_(const DataflowBlockNode* op) override {
auto block = ffi::GetRef<DataflowBlock>(op);
auto old_idxs = inplace_idxs;
// For now, only handle exact match cases.
// Note: Not passing any input values for now, as we can't make any assumptions
// about them.
auto matches_found = FindInplaceOpportunities(block, {}, builder_);
ffi::Map<Binding, ffi::Array<int64_t>> new_idxs;
for (auto match : matches_found.second) {
new_idxs.Set(block->bindings[match->binding_idx], match->arg_idxs);
}
inplace_idxs = new_idxs;
auto ret = ExprMutator::VisitBindingBlock_(op);
inplace_idxs = old_idxs;
return ret;
}
Expr ReplaceBoundCall(const Binding& binding) {
// can just pick the first index arbitrarily (only using one output for now too)
// now replace the binding appropriately
auto arg_idxs = inplace_idxs.at(binding);
auto target = GetBoundValue(binding).as_or_throw<Call>();
auto new_call = CreateInplaceCall(target, {arg_idxs[0]});
return builder_->Normalize(new_call);
}
void VisitBinding_(const VarBindingNode* binding) override {
auto binding_ref = ffi::GetRef<VarBinding>(binding);
if (!inplace_idxs.count(binding_ref)) {
ExprMutator::VisitBinding_(binding);
return;
}
Expr new_value = ReplaceBoundCall(binding_ref);
builder_->EmitNormalized(VarBinding(binding->var, new_value, binding->span));
}
void VisitBinding_(const MatchCastNode* binding) override {
auto binding_ref = ffi::GetRef<MatchCast>(binding);
if (!inplace_idxs.count(binding_ref)) {
ExprMutator::VisitBinding_(binding);
return;
}
Expr new_value = ReplaceBoundCall(binding_ref);
builder_->EmitNormalized(MatchCast(binding->var, new_value, binding->ty, binding->span));
}
// Given the call and indices of arguments that could be done in-place,
// replace the call with a call to an in-place PrimFunc.
// (Made public for testing.)
Call CreateInplaceCall(const Call& call, const ffi::Array<int64_t>& inplace_indices) {
static const auto& legalize_map = Op::GetAttrMap<FLegalize>("FLegalize");
static const auto& call_tir_inplace_op = Op::Get("relax.call_tir_inplace");
auto op = call->op.as_or_throw<Op>();
auto legalized_call = legalize_map[op](builder_, call).as_or_throw<Call>();
auto* legalized_call_cow = legalized_call.CopyOnWrite();
// The legalized call should be call_tir. We will replace it with call_tir_inplace
// and replace the called PrimFunc with an inplace version
auto legal_op = legalized_call->args[0].as_or_throw<GlobalVar>();
legalizers_added.push_back(legal_op);
auto inline_legal_op_name = legal_op->name_hint + "_inplace";
auto mod = builder_->GetContextIRModule();
auto old_primfunc = mod->Lookup(legal_op).as_or_throw<tirx::PrimFunc>();
tirx::Stmt new_body = old_primfunc->body;
size_t num_outs = inplace_indices.size();
size_t num_params = old_primfunc->params.size();
// the replacement we must make:
// 1. For each output var, replace its corresponding buffers with the corresponding inplace
// index
// var's buffers
// 2. For each output var, replace its instances with the corresponding inplace index var
// 3. Do the same for the *buffer vars* corresponding to the output vars
// 4. Remove the output vars from the param list
ffi::Map<tirx::BufferVar, tirx::BufferVar> buffer_subst_map;
ffi::Map<tirx::Var, tirx::Var> var_subst_map;
for (size_t i = 0; i < num_outs; i++) {
// we will substitute output i with the corresponding param indicated by inplace indices
auto output_var = old_primfunc->params[num_params - num_outs + i];
auto inplace_var = old_primfunc->params[inplace_indices[i]];
var_subst_map.Set(output_var, inplace_var);
// also do the same with the buffer vars
auto output_buffer = output_var.as_or_throw<tirx::BufferVar>();
auto inplace_buffer = inplace_var.as_or_throw<tirx::BufferVar>();
var_subst_map.Set(output_buffer.var(), inplace_buffer.var());
buffer_subst_map.Set(output_buffer, inplace_buffer);
}
// apply substitutions
new_body = RemapBuffers(new_body, buffer_subst_map);
new_body = tirx::Substitute(new_body,
[&var_subst_map](const tirx::Var& v) -> ffi::Optional<tvm::Expr> {
if (var_subst_map.count(v)) {
return tvm::Expr(var_subst_map.at(v));
}
return std::nullopt;
});
// now get rid of the last num_outputs arguments
// (couldn't do earlier or else it would have thrown off the indexing)
ffi::Array<tirx::Var> new_params(old_primfunc->params.begin(),
old_primfunc->params.begin() + (num_params - num_outs));
tirx::PrimFunc new_primfunc(new_params, new_body, old_primfunc->ret_type, old_primfunc->attrs,
old_primfunc->span);
// note: this might be a good time to get rid of the old legalized function, but we don't do it
// now because later ops might need the same one. Instead, we will clean up at the end
auto new_gv = builder_->AddFunction(new_primfunc, inline_legal_op_name);
// update the call (change the op, update the argument, change the attrs)
legalized_call_cow->op = call_tir_inplace_op;
ffi::Array<Expr> new_args(legalized_call->args.begin(), legalized_call->args.end());
new_args.Set(0, new_gv);
legalized_call_cow->args = new_args;
ffi::ObjectPtr<CallTIRInplaceAttrs> attrs = ffi::make_object<CallTIRInplaceAttrs>();
attrs->inplace_indices = inplace_indices;
legalized_call_cow->attrs = Attrs(attrs);
return legalized_call;
}
// Made public for testing.
IRModule CurrentMod() { return builder_->GetContextIRModule(); }
private:
const IRModule& mod_;
// Keep track of legalizers we add so we can clean up at the end.
ffi::Array<GlobalVar> legalizers_added;
// The current function's params will be treated as non-aliased
// (we are assuming good behavior on the user's part).
ffi::Array<Var> func_params;
// map of eligible bindings to indices of arguments that can be used as the in-place target
ffi::Map<Binding, ffi::Array<int64_t>> inplace_idxs;
};
namespace transform {
ffi::Map<Var, ffi::Array<int64_t>> DataflowLivenessAnalysis(const DataflowBlock& block) {
auto liveness_ranges = AnalyzeLiveness(block);
ffi::Map<Var, ffi::Array<int64_t>> ret;
for (auto kv : liveness_ranges) {
ret.Set(kv.first, {kv.second.first, kv.second.second});
}
return ret;
}
ffi::Array<ffi::ObjectRef> DataflowAliasAnalysis(const DataflowBlock& block,
ffi::Array<Var> inputs) {
AliasAnalyzer analyzer;
auto res = analyzer.Analyze(block, inputs);
auto alias_sets = res.first;
auto tuple_map = res.second;
ffi::Map<Var, ffi::Array<int64_t>> new_alias_sets;
ffi::Map<IntImm, ffi::Array<ffi::Array<int64_t>>> new_tuple_map;
for (auto kv : alias_sets) {
ffi::Array<int64_t> aliases;
for (auto alias : kv.second) {
aliases.push_back(alias);
}
new_alias_sets.Set(kv.first, aliases);
}
for (auto kv : tuple_map) {
ffi::Array<ffi::Array<int64_t>> elem_aliases;
for (auto alias_set : kv.second) {
ffi::Array<int64_t> dim_aliases;
for (auto alias : alias_set) {
dim_aliases.push_back(alias);
}
elem_aliases.push_back(dim_aliases);
}
new_tuple_map.Set(IntImm::Int32(kv.first), elem_aliases);
}
return {new_alias_sets, new_tuple_map};
}
// this would be preferable to do as a dataflow block pass,
// but the transformation adds new PrimFuncs, so it affects the module
tvm::transform::Pass DataflowUseInplaceCalls() {
return tvm::transform::CreateModulePass(
[](const IRModule& mod, const PassContext& ctx) -> IRModule {
ModuleInplaceTransformer transformer(mod);
return transformer.Transform();
},
0, "DataflowInsertInPlaceCalls", {}, false);
}
ffi::Array<ffi::Array<InplaceOpportunity>> DataflowInplaceAnalysis(const DataflowBlock& block,
const ffi::Array<Var>& inputs,
const IRModule& mod) {
auto index_lists = relax::FindInplaceOpportunities(block, inputs, BlockBuilder::Create(mod));
return {ffi::Array<InplaceOpportunity>(index_lists.first.begin(), index_lists.first.end()),
ffi::Array<InplaceOpportunity>(index_lists.second.begin(), index_lists.second.end())};
}
// these are exposed only for testing
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef()
.def("relax.testing.transform.DataflowLivenessAnalysis", DataflowLivenessAnalysis)
.def("relax.testing.transform.DataflowAliasAnalysis", DataflowAliasAnalysis)
.def("relax.testing.transform.DataflowInplaceAnalysis", DataflowInplaceAnalysis)
.def("relax.testing.transform.SingleInplaceCall",
[](const IRModule& mod, const Call& call,
const ffi::Array<int64_t>& inplace_indices) -> ffi::Array<ffi::ObjectRef> {
ModuleInplaceTransformer transformer(mod);
auto ret_call = transformer.CreateInplaceCall(call, inplace_indices);
return ffi::Array<ffi::ObjectRef>{ret_call, transformer.CurrentMod()};
});
}
// actually exposed
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def("relax.transform.DataflowUseInplaceCalls", DataflowUseInplaceCalls);
}
} // namespace transform
} // namespace relax
} // namespace tvm