blob: dabad44f55efbe7cdd664147812cec49c5f49e11 [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 ir_utils.cc
* \brief Helper functions to construct and compose IR nodes.
*/
#include "ir_utils.h"
#include <tvm/arith/analyzer.h>
#include <tvm/arith/int_solver.h>
#include <tvm/ffi/cast.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ir/scope_stack.h>
#include <tvm/s_tir/stmt.h>
#include <tvm/tirx/analysis.h>
#include <tvm/tirx/layout.h>
#include <tvm/tirx/stmt_functor.h>
#include <tvm/tirx/transform.h>
#include <unordered_map>
#include <unordered_set>
#include <utility>
namespace tvm {
namespace tirx {
Stmt MergeNest(const std::vector<Stmt>& nest, Stmt body) {
// use reverse iteration
for (auto ri = nest.rbegin(); ri != nest.rend(); ++ri) {
Stmt s = *ri;
if (const auto* for_ = s.as<ForNode>()) {
auto n = ffi::make_object<ForNode>(*for_);
TVM_FFI_ICHECK(is_no_op(n->body));
n->body = body;
body = Stmt(n);
} else if (const auto* bind = s.as<BindNode>()) {
// Bind has no body -- prepend it before the accumulated body in a SeqStmt.
body = SeqStmt::Flatten(ffi::GetRef<Stmt>(bind), body);
} else if (const auto* attr = s.as<AttrStmtNode>()) {
auto n = ffi::make_object<AttrStmtNode>(*attr);
TVM_FFI_ICHECK(is_no_op(n->body));
n->body = body;
body = Stmt(n);
} else if (const auto* ite = s.as<IfThenElseNode>()) {
auto n = ffi::make_object<IfThenElseNode>(*ite);
TVM_FFI_ICHECK(is_no_op(n->then_case));
TVM_FFI_ICHECK(!n->else_case);
n->then_case = body;
body = Stmt(n);
} else if (const auto* seq = s.as<SeqStmtNode>()) {
auto n = ffi::make_object<SeqStmtNode>(*seq);
TVM_FFI_ICHECK(n->size() != 0 && is_no_op(n->seq[n->size() - 1]));
n->seq.Set(n->size() - 1, body);
body = Stmt(n);
} else if (s.as<AssertStmtNode>()) {
body = SeqStmt({s, body});
} else if (s.as<AllocBufferNode>() || s.as<DeclBufferNode>()) {
body = SeqStmt::Flatten(s, body);
} else {
TVM_FFI_THROW(InternalError) << "not supported nest type";
}
}
return body;
}
Stmt MergeNest(const std::vector<std::vector<Stmt>>& nest, Stmt body) {
for (auto ri = nest.rbegin(); ri != nest.rend(); ++ri) {
body = MergeNest(*ri, body);
}
return body;
}
class IRConvertSSA final : public StmtExprMutator {
public:
PrimFunc VisitPrimFunc(PrimFunc func) {
// Remap parameters, if they were used in another function.
// Function-scope remaps use function_scope_var_remap_ (not the scope stack),
// because they persist across the entire function body.
auto params = func->params.Map([&](const tirx::Var& var) -> tirx::Var {
if (defined_.count(var.get())) {
Var new_var = MakeNewVar(var);
PushVarRemap(var, new_var);
return new_var;
} else {
defined_.insert(var.get());
return var;
}
});
// Remap implicitly defined buffer parameters
{
std::unordered_set<const VarNode*> defined_params;
for (const auto& var : func->params) {
defined_params.insert(var.get());
}
std::unordered_set<const VarNode*> defined_match_vars;
for (const Var& param : func->params) {
auto buffer = param.as<BufferVar>();
if (!buffer) continue;
auto check_var = [&](const Var& var) {
const VarNode* var_ptr = var.get();
if (defined_params.count(var_ptr)) return;
if (!defined_match_vars.insert(var_ptr).second) return;
// Buffer-parameter shape vars use "match" semantics: first occurrence
// defines the var, subsequent occurrences (in other buffers) are
// just consistent uses of the same var -- not redefinitions.
if (defined_.count(var_ptr)) {
Var new_var = MakeNewVar(var);
PushVarRemap(var, new_var);
} else {
defined_.insert(var_ptr);
}
};
for (const auto& dim : buffer.value()->shape) {
PostOrderVisit(dim, [&](const ffi::ObjectRef& obj) {
if (auto var = obj.as<Var>()) check_var(var.value());
});
}
for (const auto& stride : buffer.value()->strides) {
if (auto var = stride.as<Var>()) check_var(var.value());
}
if (auto var = buffer.value()->elem_offset.as<Var>()) check_var(var.value());
}
}
// Update the buffer parameters, based on the redefined parameters
bool buffer_params_changed = false;
for (size_t i = 0; i < func->params.size(); ++i) {
if (auto buffer = func->params[i].as<BufferVar>()) {
BufferVar new_buffer = GetRemappedBuffer(buffer.value());
if (!new_buffer.same_as(buffer.value()) || !params[i].same_as(new_buffer)) {
buffer_params_changed = true;
params.Set(i, new_buffer.var());
}
}
}
auto attrs = [&]() -> DictAttrs {
ffi::Map<ffi::String, ffi::Any> dict;
bool made_change = false;
for (const auto& [key, old_value] : func->attrs->dict) {
auto value = old_value;
if (auto expr = value.as<PrimExpr>()) {
value = VisitPrimExpr(expr.value());
} else if (auto* stmt = value.as<StmtNode>()) {
value = VisitStmt(ffi::GetRef<Stmt>(stmt));
}
made_change = made_change || !value.same_as(old_value);
dict.Set(key, value);
}
if (made_change) {
return DictAttrs(dict);
} else {
return func->attrs;
}
}();
auto body = VisitStmt(func->body);
// If anything changed, update the returned function
if (!params.same_as(func->params) || buffer_params_changed || !attrs.same_as(func->attrs) ||
!body.same_as(func->body)) {
func = PrimFunc(params, body, func->ret_type, attrs);
}
// Pop function-scope remaps in reverse order
PopAllRemapsInCurrentScope();
function_scope_var_remap_.clear();
return func;
}
// Do not use the base VisitBufferDef for buffer remapping.
//
// IRConvertSSA has its own scoped buffer remapping via GetRemappedBuffer and
// buf_remap_, which handles SSA conversion of buffer data vars, shape, strides,
// and elem_offset with proper scope tracking. The base StmtMutator::VisitBufferDef
// would create a conflicting second remap (into base buffer_remap_) when called
// from the default DeclBuffer/AllocBuffer handlers, producing buffers with
// undefined SSA-renamed variables.
BufferVar VisitBufferDef(const BufferVar& buffer, bool alloc_data) override { return buffer; }
Expr VisitExpr_(const VarNode* op) final { return GetRemappedVar(ffi::GetRef<Var>(op)); }
Expr VisitExpr_(const LetNode* op) final {
const Var& v = op->var;
if (defined_.count(v.get())) {
PrimExpr value = this->VisitPrimExpr(op->value);
Var new_var = MakeNewVar(v);
PushVarRemap(v, new_var);
PrimExpr body = this->VisitPrimExpr(op->body);
PopVarRemap(v, new_var);
return Let(new_var, value, body);
} else {
defined_.insert(v.get());
return StmtExprMutator::VisitExpr_(op);
}
}
Expr VisitExpr_(const BufferLoadNode* op) final {
auto node = StmtExprMutator::VisitExpr_(op).as_or_throw<BufferLoad>();
auto output = VisitBufferAccess(std::move(node));
return output;
}
Stmt VisitStmt_(const BufferStoreNode* op) final {
auto node = StmtExprMutator::VisitStmt_(op).as_or_throw<BufferStore>();
auto output = VisitBufferAccess(std::move(node));
return output;
}
Stmt VisitStmt_(const DeclBufferNode* op) final {
Var v = op->buffer.var();
if (defined_.count(v.get())) {
Var new_var = MakeNewVar(v);
PushVarRemap(v, new_var);
} else {
defined_.insert(v.get());
}
DeclBuffer decl = StmtExprMutator::VisitStmt_(op).as_or_throw<DeclBuffer>();
BufferVar new_buffer = GetRemappedBuffer(decl->buffer);
if (!new_buffer.same_as(decl->buffer)) {
decl.CopyOnWrite()->buffer = std::move(new_buffer);
}
return decl;
}
Stmt VisitStmt_(const SBlockNode* op) final {
SBlock block = ffi::GetRef<SBlock>(op);
// The SBlockNode is the point of definition for the IterVar
// instances. These re-defines must be present before visiting
// the body of the SBlockNode.
return scope_.WithNewScope([&]() -> Stmt {
ffi::Array<IterVar> iter_vars = op->iter_vars.Map([&](IterVar iter_var) {
if (defined_.count(iter_var->var.get())) {
Var new_var = MakeNewVar(iter_var->var);
PushVarRemap(iter_var->var, new_var);
iter_var.CopyOnWrite()->var = new_var.as_or_throw<PrimVar>();
} else {
defined_.insert(iter_var->var.get());
}
return iter_var;
});
ffi::Array<BufferRegion> reads =
block->reads.Map([&](const auto& region) { return VisitBufferAccess(region); });
ffi::Array<BufferRegion> writes =
block->writes.Map([&](const auto& region) { return VisitBufferAccess(region); });
if (!reads.same_as(block->reads) || !writes.same_as(block->writes) ||
!iter_vars.same_as(op->iter_vars)) {
auto write_ptr = block.CopyOnWrite();
write_ptr->reads = reads;
write_ptr->writes = writes;
write_ptr->iter_vars = iter_vars;
}
return StmtExprMutator::VisitStmt_(block.get()).as_or_throw<SBlock>();
});
}
template <typename Node>
Node VisitBufferAccess(Node node) {
BufferVar new_buf = GetRemappedBuffer(node->buffer);
if (!new_buf.same_as(node->buffer)) {
auto writer = node.CopyOnWrite();
writer->buffer = new_buf;
}
return node;
}
Var GetRemappedVar(Var var) {
if (auto it = var_remap_.find(var.get()); it != var_remap_.end() && it->second.size()) {
return it->second.back();
} else if (auto it = function_scope_var_remap_.find(var.get());
it != function_scope_var_remap_.end()) {
return it->second;
} else {
return var;
}
}
BufferVar GetRemappedBuffer(BufferVar buf) {
// Determine the buffer var that should be in the updated buffer,
// given the current scope. If no redefines are present, then the
// buffer var is unchanged.
Var new_buffer_var = GetRemappedVar(buf.var());
PrimExpr elem_offset = VisitPrimExpr(buf->elem_offset);
auto visit_expr = [this](const PrimExpr& expr) { return VisitPrimExpr(expr); };
ffi::Array<PrimExpr> shape = buf->shape.Map(visit_expr);
ffi::Array<PrimExpr> strides = buf->strides.Map(visit_expr);
// Rewrite the layout's per-iter extent/stride expressions in lockstep
// with the shape. If we don't, SSA-renamed shape vars end up as fresh
// Vars while the layout still references the original, producing
// structurally-unequal buffers whose shape and layout disagree (e.g.,
// test_dynamic_launch_thread).
ffi::Optional<Layout> new_layout = buf->layout;
bool layout_changed = false;
if (buf->layout.has_value()) {
if (auto opt_tile = buf->layout.value().as<TileLayoutNode>()) {
auto remap_iter = [&](const Iter& it) -> Iter {
PrimExpr new_extent = VisitPrimExpr(it->extent);
PrimExpr new_stride = VisitPrimExpr(it->stride);
if (new_extent.same_as(it->extent) && new_stride.same_as(it->stride)) {
return it;
}
return Iter(new_extent, new_stride, it->axis);
};
auto new_shard = opt_tile->shard.Map(remap_iter);
auto new_replica = opt_tile->replica.Map(remap_iter);
if (!new_shard.same_as(opt_tile->shard) || !new_replica.same_as(opt_tile->replica)) {
new_layout = TileLayout(new_shard, new_replica, opt_tile->offset);
layout_changed = true;
}
}
}
// If no mapping is required, return the original buffer.
if (new_buffer_var.same_as(buf.var()) && elem_offset.same_as(buf->elem_offset) &&
shape.same_as(buf->shape) && strides.same_as(buf->strides) && !layout_changed) {
return buf;
}
// If the current scope already has a mapping of this buffer, use
// the mapped buffer.
auto key = buf.get();
std::vector<BufferVar>& buffers = buf_remap_[key];
if (buffers.size() && buffers.back().same_as(new_buffer_var)) {
return buffers.back();
}
// When only the buffer's identity changed, the remapped Var already has
// the desired BufferType. Reuse that exact Var so the definition and all
// subsequent uses remain in SSA.
if (const auto* type = new_buffer_var->ty.as<BufferTypeNode>()) {
BufferVar candidate(new_buffer_var);
if (shape.same_as(type->shape) && strides.same_as(type->strides) &&
elem_offset.same_as(type->elem_offset) && !layout_changed) {
buffers.push_back(candidate);
return candidate;
}
}
// Otherwise, make and return a new buffer object that uses the
// new buffer, pushing it onto the scoped stack of existing
// buffers. This will be popped when the new_buffer_var
// redefinition is popped.
auto type = CopyBufferType(buf);
type->shape = shape;
type->strides = strides;
type->elem_offset = elem_offset;
if (layout_changed) {
type->layout = std::move(new_layout);
}
BufferVar new_buf = RebuildBufferVar(buf, std::move(type), new_buffer_var->name);
// A BufferVar's metadata lives in its Var type. If rewriting the
// metadata required a fresh Var, make it the active remap as well. This
// keeps BufferLoad/BufferStore and ordinary Var uses (such as
// buffer_data) on the same identity.
auto it = var_remap_.find(buf.get());
if (it != var_remap_.end() && it->second.size() && it->second.back().same_as(new_buffer_var)) {
it->second.back() = new_buf.var();
} else if (auto function_it = function_scope_var_remap_.find(buf.get());
function_it != function_scope_var_remap_.end() &&
function_it->second.same_as(new_buffer_var)) {
function_it->second = new_buf.var();
} else {
PushVarRemap(buf.var(), new_buf.var());
}
buffers.push_back(new_buf);
return new_buf;
}
Stmt VisitStmt_(const BindNode* op) final {
// Bind var remaps are tracked in the current scope so they persist
// across SeqStmt siblings and are cleaned up when the enclosing
// body-carrying statement's scope exits.
const Var& v = op->var;
if (defined_.count(v.get())) {
Expr value = this->VisitExpr(op->value);
Var new_var = MakeNewVar(v);
PushVarRemap(v, new_var);
return Bind(new_var, value);
} else {
defined_.insert(v.get());
return StmtExprMutator::VisitStmt_(op);
}
}
Stmt VisitStmt_(const IfThenElseNode* op) final {
// Each branch gets its own scope so Bind remaps in one branch
// do not leak into the other.
PrimExpr condition = VisitPrimExpr(op->condition);
Stmt then_case = scope_.WithNewScope([&]() -> Stmt { return VisitStmt(op->then_case); });
ffi::Optional<Stmt> else_case;
if (op->else_case) {
else_case = scope_.WithNewScope([&]() -> Stmt { return VisitStmt(op->else_case.value()); });
}
if (condition.same_as(op->condition) && then_case.same_as(op->then_case) &&
else_case.same_as(op->else_case)) {
return ffi::GetRef<Stmt>(op);
}
return IfThenElse(condition, then_case, else_case);
}
Stmt VisitStmt_(const ForNode* op) final {
const Var& v = op->loop_var;
if (defined_.count(v.get())) {
return scope_.WithNewScope([&]() -> Stmt {
Var new_var = MakeNewVar(v);
PushVarRemap(v, new_var);
Stmt stmt = StmtExprMutator::VisitStmt_(op);
auto n = ffi::make_object<ForNode>(*stmt.as<ForNode>());
n->loop_var = new_var.as_or_throw<PrimVar>();
return For(n);
});
} else {
defined_.insert(v.get());
return scope_.WithNewScope([&]() -> Stmt { return StmtExprMutator::VisitStmt_(op); });
}
}
Stmt VisitStmt_(const WhileNode* op) final {
return scope_.WithNewScope([&]() -> Stmt { return StmtExprMutator::VisitStmt_(op); });
}
Stmt VisitStmt_(const AllocBufferNode* op) final {
Var v = op->buffer.var();
if (defined_.count(v.get())) {
Var new_var = MakeNewVar(v);
PushVarRemap(v, new_var);
} else {
defined_.insert(v.get());
}
Stmt stmt = StmtExprMutator::VisitStmt_(op);
op = stmt.as<AllocBufferNode>();
// Use GetRemappedBuffer so that the AllocBuffer's buffer is the same
// object as the one used by BufferStore/BufferLoad in subsequent siblings.
BufferVar new_buf = GetRemappedBuffer(op->buffer);
if (!new_buf.same_as(op->buffer)) {
auto node = stmt.as_or_throw<AllocBuffer>();
node.CopyOnWrite()->buffer = std::move(new_buf);
return node;
}
return stmt;
}
Stmt VisitStmt_(const AttrStmtNode* op) final {
if (const IterVarNode* iter_var = op->node.as<IterVarNode>()) {
Range dom = iter_var->dom;
if (dom.defined()) {
auto min = VisitPrimExpr(dom->min);
auto extent = VisitPrimExpr(dom->extent);
if (!min.same_as(iter_var->dom->min) || !extent.same_as(iter_var->dom->extent)) {
dom = Range::FromMinExtent(min, extent);
}
}
Var var = iter_var->var;
bool delayed_define = false;
if (auto it = function_scope_var_remap_.find(var.get());
it != function_scope_var_remap_.end()) {
var = it->second;
} else if (defined_.count(var.get())) {
Var new_var(var->name, var->ty);
function_scope_var_remap_.insert({var.get(), new_var});
var = new_var;
} else {
// The AttrStmt refers to an undefined variable. This is
// allowed for some attributes, such as
// "pragma_parallel_launch_point", which annotates a variable
// that is about to occur in a ForNode. In these cases, the
// ForNode and the AttrStmt must continue using the same
// variable defintion.
//
// However, other AttrStmt, such as "thread_extent", act as
// points of definition for the variable they annotate. If
// the variable has not been defined after visiting the body,
// we should mark it as defined before exiting. This ensures
// correct de-duplication between multiple functions.
//
// This implementation may be simplified in the future by
// moving "pragma_parallel_launch_point" to be an annotation
// on the `ForNode`, rather than an `AttrStmt`.
delayed_define = true;
}
IterVar new_iter_var;
if (dom.same_as(iter_var->dom) && var.same_as(iter_var->var)) {
new_iter_var = ffi::GetRef<IterVar>(iter_var);
} else {
new_iter_var = IterVar(dom, var.as_or_throw<PrimVar>(), iter_var->iter_type,
iter_var->thread_tag, iter_var->span);
}
auto value = VisitPrimExpr(op->value);
auto body = scope_.WithNewScope([&]() -> Stmt { return VisitStmt(op->body); });
Stmt output;
if (new_iter_var.get() == iter_var && body.same_as(op->body) && value.same_as(op->value)) {
output = ffi::GetRef<Stmt>(op);
} else {
output = AttrStmt(new_iter_var, op->attr_key, value, body, iter_var->span);
}
if (delayed_define) {
if (!defined_.count(var.get())) {
function_scope_var_remap_.insert({var.get(), var});
defined_.insert(var.get());
}
}
return output;
} else if (const VarNode* v = op->node.as<VarNode>()) {
Stmt stmt = scope_.WithNewScope([&]() -> Stmt { return StmtExprMutator::VisitStmt_(op); });
op = stmt.as<AttrStmtNode>();
if (var_remap_.count(v) && var_remap_[v].size() != 0) {
return AttrStmt(var_remap_[v].back(), op->attr_key, op->value, op->body);
} else {
return stmt;
}
} else {
return scope_.WithNewScope([&]() -> Stmt { return StmtExprMutator::VisitStmt_(op); });
}
}
private:
/*! \brief Record of a variable remap pushed to the current scope. */
struct VarRemap {
Var old_var;
Var new_var;
};
/*! \brief Check whether a buffer uses a variable in any remapped field. */
static bool BufferDependsOnVar(const BufferVar& buffer, const VarNode* var) {
if (buffer.get() == var) return true;
auto uses_var = [var](const PrimExpr& expr) {
return expr.defined() && UsesVar(expr, [var](const VarNode* node) { return node == var; });
};
if (uses_var(buffer->elem_offset)) return true;
for (const PrimExpr& dim : buffer->shape) {
if (uses_var(dim)) return true;
}
for (const PrimExpr& stride : buffer->strides) {
if (uses_var(stride)) return true;
}
if (buffer->layout.has_value()) {
if (const auto* tile_layout = buffer->layout.value().as<TileLayoutNode>()) {
for (const Iter& iter : tile_layout->shard) {
if (uses_var(iter->extent) || uses_var(iter->stride)) return true;
}
for (const Iter& iter : tile_layout->replica) {
if (uses_var(iter->extent) || uses_var(iter->stride)) return true;
}
}
}
return false;
}
/*! \brief Create a new variable with the same name and type as the original. */
static Var MakeNewVar(const Var& old_var) { return Var(old_var->name, old_var->ty); }
/*! \brief Push a variable remap to the current scope and the var_remap_ stack. */
void PushVarRemap(const Var& old_var, const Var& new_var) {
var_remap_[old_var.get()].push_back(new_var);
auto& level = scope_.Current();
level.parent = this;
level.push_back({old_var, new_var});
}
/*! \brief Pop a single variable remap (used for expression-level Let scoping). */
void PopVarRemap(const Var& old_var, const Var& new_var) {
var_remap_[old_var.get()].pop_back();
for (auto& kv : buf_remap_) {
std::vector<BufferVar>& buffers = kv.second;
if (buffers.size() && BufferDependsOnVar(buffers.back(), new_var.get())) {
buffers.pop_back();
}
}
// Also remove from the current scope's tracking vector
auto& current = scope_.Current();
if (current.size() && current.back().new_var.same_as(new_var)) {
current.pop_back();
}
}
/*! \brief Pop all remaps in the current scope level (used for function-scope cleanup). */
void PopAllRemapsInCurrentScope() {
auto& current = scope_.Current();
while (current.size()) {
auto& remap = current.back();
var_remap_[remap.old_var.get()].pop_back();
for (auto& kv : buf_remap_) {
std::vector<BufferVar>& buffers = kv.second;
if (buffers.size() && BufferDependsOnVar(buffers.back(), remap.new_var.get())) {
buffers.pop_back();
}
}
current.pop_back();
}
}
/*! \brief Scope stack: each scope level holds the remaps introduced in that scope.
*
* When a body-carrying statement (For, SBlock, Allocate) calls
* scope_.WithNewScope([&]{...}), a new scope level is pushed.
* Bind statements push their remaps to the current scope.
* On scope exit, the destructor of std::vector<VarRemap> triggers,
* and we undo all remaps in that level.
*
* Note: ScopeStack<T>::WithNewScope calls T's destructor on exit.
* std::vector's destructor destroys elements but does NOT call custom
* cleanup. So we wrap the vector in ScopeLevel which handles cleanup.
*/
struct ScopeLevel {
std::vector<VarRemap> remaps;
IRConvertSSA* parent{nullptr};
void push_back(VarRemap remap) { remaps.push_back(std::move(remap)); }
size_t size() const { return remaps.size(); }
VarRemap& back() { return remaps.back(); }
void pop_back() { remaps.pop_back(); }
~ScopeLevel() {
if (!parent) return;
// Pop remaps in reverse order
while (remaps.size()) {
auto& remap = remaps.back();
parent->var_remap_[remap.old_var.get()].pop_back();
for (auto& kv : parent->buf_remap_) {
std::vector<BufferVar>& buffers = kv.second;
if (buffers.size() && BufferDependsOnVar(buffers.back(), remap.new_var.get())) {
buffers.pop_back();
}
}
remaps.pop_back();
}
}
ScopeLevel() = default;
ScopeLevel(const ScopeLevel&) = delete;
ScopeLevel& operator=(const ScopeLevel&) = delete;
ScopeLevel(ScopeLevel&& other) noexcept
: remaps(std::move(other.remaps)), parent(other.parent) {
other.parent = nullptr; // prevent other's destructor from popping
}
ScopeLevel& operator=(ScopeLevel&& other) noexcept {
if (this != &other) {
// Run our destructor logic first
if (parent) {
while (remaps.size()) {
auto& remap = remaps.back();
parent->var_remap_[remap.old_var.get()].pop_back();
for (auto& kv : parent->buf_remap_) {
std::vector<BufferVar>& buffers = kv.second;
if (buffers.size() && BufferDependsOnVar(buffers.back(), remap.new_var.get())) {
buffers.pop_back();
}
}
remaps.pop_back();
}
}
remaps = std::move(other.remaps);
parent = other.parent;
other.parent = nullptr;
}
return *this;
}
};
std::unordered_map<const VarNode*, std::vector<Var>> var_remap_;
std::unordered_set<const VarNode*> defined_;
std::unordered_map<const VarNode*, std::vector<BufferVar>> buf_remap_;
std::unordered_map<const VarNode*, Var> function_scope_var_remap_;
ScopeStack<ScopeLevel> scope_;
};
Stmt ConvertSSA(Stmt stmt) { return IRConvertSSA()(std::move(stmt)); }
ffi::String GetPtrStorageScope(Var buffer_var) {
if (const auto* buffer_type = buffer_var->ty.as<BufferTypeNode>()) {
return buffer_type->storage_scope;
}
const auto* ptr_type = buffer_var->ty.as<PointerTypeNode>();
TVM_FFI_ICHECK(ptr_type)
<< "The provided variable is neither a pointer nor a buffer-typed variable";
return ptr_type->storage_scope;
}
ffi::Array<PrimExpr> GetBufferAllocationShape(const BufferVar& buffer) {
ffi::Array<PrimExpr> alloc_shape = buffer->shape;
if (buffer->strides.size()) {
TVM_FFI_ICHECK_EQ(buffer->shape.size(), buffer->strides.size());
for (size_t i = buffer->strides.size() - 1; i > 0; --i) {
TVM_FFI_ICHECK(arith::Analyzer()->CanProveEqual(
floormod(buffer->strides[i - 1], buffer->strides[i]), 0));
alloc_shape.Set(i, buffer->strides[i - 1] / buffer->strides[i]);
}
}
return alloc_shape;
}
ffi::Array<PrimExpr> ConvertIndices(const MatchBufferRegion& match_buffer,
const ffi::Array<PrimExpr>& indices) {
const BufferVar& target = match_buffer->buffer;
const BufferRegion& source = match_buffer->source;
TVM_FFI_ICHECK_EQ(indices.size(), target->shape.size());
arith::Analyzer analyzer;
ffi::Array<PrimExpr> result;
result.reserve(source->region.size());
size_t offset = source->region.size() - indices.size();
for (size_t i = 0; i < offset; ++i) {
const Range& range = source->region[i];
TVM_FFI_ICHECK(analyzer->CanProve(range->extent == 1));
result.push_back(range->min);
}
for (size_t i = 0; i < indices.size(); ++i) {
const Range& range = source->region[i + offset];
const PrimExpr& index = indices[i];
result.push_back(range->min + index);
}
return result;
}
Region ConvertRegion(const MatchBufferRegion& match_buffer, const Region& region) {
const BufferVar& target = match_buffer->buffer;
const BufferRegion& source = match_buffer->source;
TVM_FFI_ICHECK_EQ(region.size(), target->shape.size());
arith::Analyzer analyzer;
Region result;
result.reserve(source->region.size());
size_t offset = source->region.size() - region.size();
for (size_t i = 0; i < offset; ++i) {
const Range& source_range = source->region[i];
TVM_FFI_ICHECK(analyzer->CanProve(source_range->extent == 1));
result.push_back(Range::FromMinExtent(source_range->min, 1));
}
for (size_t i = 0; i < region.size(); ++i) {
const Range& source_range = source->region[i + offset];
const Range& target_range = region[i];
result.push_back(
Range::FromMinExtent(source_range->min + target_range->min, target_range->extent));
}
return result;
}
ffi::Optional<arith::IntConstraints> ConditionalBoundsContext::TrySolveCondition() {
// extract equations and related vars from condition expression.
// currently only extract simple integral equations which could be solvable.
arith::Analyzer analyzer;
PrimExpr condition = analyzer->Simplify(condition_);
if (is_const_int(condition)) {
return std::nullopt;
}
ffi::Array<PrimExpr> equations;
ffi::Array<PrimVar> vars;
std::function<void(const PrimExpr&)> fvisit = [&equations, &vars, &fvisit](const PrimExpr& e) {
if (e->IsInstance<GENode>() || e->IsInstance<GTNode>() || e->IsInstance<LENode>() ||
e->IsInstance<LTNode>() || e->IsInstance<EQNode>() || e->IsInstance<NENode>()) {
bool is_simple = true;
std::vector<PrimVar> cand_vars;
PostOrderVisit(e, [&cand_vars, &is_simple, &e](const ffi::ObjectRef& obj) {
if (obj.same_as(e)) {
return;
} else if (const VarNode* var = obj.as<VarNode>()) {
PrimType var_ty = var->ty.as_or_throw<PrimType>();
if (var_ty.MatchesCode(DLDataTypeCode::kDLInt, DLDataTypeCode::kDLUInt)) {
cand_vars.push_back(ffi::GetRef<Var>(var).as_or_throw<PrimVar>());
}
} else {
is_simple &= obj->IsInstance<AddNode>() || obj->IsInstance<SubNode>() ||
obj->IsInstance<MulNode>() || obj->IsInstance<FloorDivNode>() ||
obj->IsInstance<FloorModNode>() || obj->IsInstance<IntImmNode>();
}
});
if (is_simple && !cand_vars.empty()) {
for (const PrimVar& new_var : cand_vars) {
if (!std::any_of(vars.begin(), vars.end(),
[&new_var](const PrimVar& v) { return v.same_as(new_var); })) {
vars.push_back(new_var);
}
}
equations.push_back(e.as_or_throw<PrimExpr>());
}
} else if (e->IsInstance<AndNode>()) {
And op = e.as_or_throw<And>();
fvisit(op->a);
fvisit(op->b);
} else if (e->IsInstance<CallNode>()) {
Call op = e.as_or_throw<Call>();
if (op->op.same_as(builtin::likely())) {
fvisit(op->args[0].as_or_throw<PrimExpr>());
}
}
};
fvisit(condition);
if (equations.empty() || vars.empty()) {
return std::nullopt;
}
// build dom ranges for related vars
ffi::Map<Var, Range> ranges;
for (const Var& v : vars) {
arith::IntSet dom;
auto relax_it = relax_map_->find(v.get());
if (relax_it != relax_map_->end()) {
dom = relax_it->second;
} else {
auto hint_it = hint_map_->find(v.get());
if (hint_it != hint_map_->end()) {
dom = hint_it->second;
}
}
if (dom.defined()) {
ranges.Set(v, Range::FromMinExtent(dom.min(), analyzer->Simplify(dom.max() - dom.min() + 1)));
}
}
// solve constraints
arith::IntConstraints constraint(vars, ranges, equations);
arith::IntConstraints result = arith::SolveInequalitiesToRange(constraint);
if (!result->relations.empty()) {
return std::nullopt;
}
return result;
}
ConditionalBoundsContext::ConditionalBoundsContext(
const PrimExpr& condition, std::unordered_map<const VarNode*, arith::IntSet>* relax_map,
std::unordered_map<const VarNode*, arith::IntSet>* hint_map,
std::vector<PrimExpr>* pending_conditions)
: condition_(condition),
relax_map_(relax_map),
hint_map_(hint_map),
pending_conditions_(pending_conditions),
origin_pending_conditions_num_(pending_conditions->size()) {}
void ConditionalBoundsContext::EnterWithScope() {
ffi::Optional<arith::IntConstraints> constraints = TrySolveCondition();
if (!constraints.has_value()) {
// fail to process the condition, add to unresolved
pending_conditions_->push_back(condition_);
return;
}
// update solved var ranges
for (const auto& kv : constraints.value()->ranges) {
const VarNode* var = kv.first.get();
arith::IntSet new_dom = arith::IntSet::FromRange(kv.second);
auto relax_it = relax_map_->find(var);
if (relax_it != relax_map_->end()) {
// this is a bound for relaxed var
origin_map_.emplace(var, relax_it->second);
relax_it->second = arith::Intersect({relax_it->second, new_dom});
} else {
// this is a bound for free var
auto hint_it = hint_map_->find(var);
if (hint_it != hint_map_->end()) {
origin_map_.emplace(var, hint_it->second);
hint_it->second = arith::Intersect({hint_it->second, new_dom});
} else {
origin_map_.emplace(var, arith::IntSet::Nothing());
hint_map_->insert(hint_it, {var, new_dom});
}
}
}
}
void ConditionalBoundsContext::ExitWithScope() {
pending_conditions_->resize(origin_pending_conditions_num_);
for (const auto& p : origin_map_) {
const auto* var = p.first;
auto relax_it = relax_map_->find(var);
if (relax_it != relax_map_->end()) {
// recover bound for relaxed var
relax_it->second = p.second;
} else {
// recover bound for free var
auto hint_it = hint_map_->find(var);
TVM_FFI_ICHECK(hint_it != hint_map_->end());
if (p.second.IsNothing()) {
hint_map_->erase(hint_it);
} else {
hint_it->second = p.second;
}
}
}
}
std::pair<PrimExpr, PrimExpr> GetAsyncWaitAttributes(const AttrStmtNode* op) {
TVM_FFI_ICHECK(op && op->attr_key == s_tir::attr::async_wait_queue_scope);
auto inner = op->body.as<AttrStmtNode>();
TVM_FFI_ICHECK(inner && inner->attr_key == s_tir::attr::async_wait_inflight_count);
return std::make_pair(op->value, inner->value);
}
/*! \brief Collect storage alignment information from annotations. */
class StorageAlignCollector : public StmtVisitor {
private:
friend std::unordered_map<Var, StorageAlignAnnotation> CollectStorageAlignAnnotation(
const Stmt& body);
/*! \brief For s-stir, the alignment annotations reside in block annotations. */
void VisitStmt_(const SBlockNode* op) final {
auto it = op->annotations.find(s_tir::attr::buffer_dim_align);
if (it != op->annotations.end()) {
auto storage_align_annotation = (*it).second.as_or_throw<StorageAlignAnnotation>();
for (const auto& storage_align_tuple : storage_align_annotation) {
int buffer_index = storage_align_tuple.get<0>();
const BufferVar& buffer = op->writes[buffer_index]->buffer;
storage_align_[buffer.var()].push_back(storage_align_tuple);
}
}
StmtVisitor::VisitStmt_(op);
}
/*! \brief AllocBuffer: check for buffer_dim_align annotations. */
void VisitStmt_(const AllocBufferNode* op) final {
auto it = op->annotations.find(s_tir::attr::buffer_dim_align);
if (it != op->annotations.end()) {
auto storage_align_annotation = (*it).second.as_or_throw<StorageAlignAnnotation>();
for (const auto& storage_align_tuple : storage_align_annotation) {
int buffer_index = storage_align_tuple.get<0>();
// the first buffer idx info is meaningless for alloc
// stmt and should set as negative intentionally.
TVM_FFI_ICHECK_EQ(buffer_index, -1);
storage_align_[op->buffer.var()].push_back(storage_align_tuple);
}
}
StmtVisitor::VisitStmt_(op);
}
/*! \brief The map from buffer var to its storage alignment information. */
std::unordered_map<Var, StorageAlignAnnotation> storage_align_;
};
std::unordered_map<Var, StorageAlignAnnotation> CollectStorageAlignAnnotation(const Stmt& body) {
StorageAlignCollector collector;
collector(body);
return std::move(collector.storage_align_);
}
int Stoi(const std::string& str) {
try {
return std::stoi(str);
} catch (std::invalid_argument& e) {
TVM_FFI_THROW(InternalError) << "Cannot convert \"" << str << "\" to int";
throw;
}
}
std::pair<int32_t, int32_t> GetWmmaFragmentDimSize(const std::string& shape_str,
const std::string& scope) {
size_t m, n, k;
size_t last_pos = 0, pos = 0;
pos = shape_str.find(", ", last_pos);
m = Stoi(shape_str.substr(last_pos, pos - last_pos));
last_pos = pos + 2;
pos = shape_str.find(", ", last_pos);
n = Stoi(shape_str.substr(last_pos, pos - last_pos));
last_pos = pos + 2;
k = Stoi(shape_str.substr(last_pos, shape_str.length() - last_pos));
if (scope == "wmma.matrix_a") {
return std::pair<int32_t, int32_t>(m, k);
} else if (scope == "wmma.matrix_b") {
return std::pair<int32_t, int32_t>(k, n);
} else if (scope == "wmma.accumulator") {
return std::pair<int32_t, int32_t>(m, n);
}
return std::pair<int32_t, int32_t>(0, 0);
}
std::optional<bool> IsHostFunc(const PrimFunc& func) {
if (func->HasNonzeroAttr(tvm::tirx::attr::kIsHostFunc)) {
return true;
} else if (auto target = func->GetAttr<Target>(tvm::attr::kTarget)) {
return target.value()->HasKey("cpu");
} else {
return std::nullopt;
}
}
namespace transform {
Pass ConvertSSA() {
auto pass_func = [](IRModule mod, PassContext ctx) {
tirx::IRConvertSSA converter;
ffi::Map<GlobalVar, BaseFunc> functions;
bool made_change = false;
for (auto [gvar, base_func] : mod->functions) {
if (auto* ptr = base_func.as<tirx::PrimFuncNode>()) {
auto updated = converter.VisitPrimFunc(ffi::GetRef<tirx::PrimFunc>(ptr));
if (!updated.same_as(base_func)) {
made_change = true;
base_func = updated;
}
}
functions.Set(gvar, base_func);
}
if (made_change) {
mod.CopyOnWrite()->functions = std::move(functions);
}
return mod;
};
return tvm::transform::CreateModulePass(pass_func, 0, "tirx.ConvertSSA", {});
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def("tirx.transform.ConvertSSA", ConvertSSA);
}
} // namespace transform
} // namespace tirx
} // namespace tvm