blob: ab1b062ad647b5096afff70081e2a4aba10fdcee [file] [log] [blame]
/*
* 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 storage_flatten.cc
* \brief Flattens storage from multi-dimensional array to 1D buffer access
*/
// The pass definition originates from Halide pipeline.
#include <tvm/arith/analyzer.h>
#include <tvm/runtime/device_api.h>
#include <tvm/runtime/registry.h>
#include <tvm/target/target_info.h>
#include <tvm/te/operation.h>
#include <tvm/tir/buffer.h>
#include <tvm/tir/builtin.h>
#include <tvm/tir/expr.h>
#include <tvm/tir/op.h>
#include <tvm/tir/stmt.h>
#include <tvm/tir/stmt_functor.h>
#include <tvm/tir/transform.h>
#include <unordered_map>
#include <unordered_set>
#include "../../arith/ir_visitor_with_analyzer.h"
#include "../../runtime/thread_storage_scope.h"
#include "arg_binder.h"
#include "ir_utils.h"
namespace tvm {
namespace tir {
using arith::IRVisitorWithAnalyzer;
using runtime::StorageRank;
using runtime::StorageScope;
using runtime::ThreadScope;
/* Make buffer realize extents and buffer shapes consistent
*
* For external buffers, verify that the extents of BufferRealize
* nodes match the shape of the external buffer. For internal
* buffers, rewrite the shape of the Buffer objects to match the
* extent of the BufferRealize, and rewrite indices of
* BufferLoad/BufferStore nodes to match.
*/
class BufferShapeLegalize : public StmtExprMutator {
public:
static transform::Pass Pass() {
auto pass_func = [](PrimFunc func, IRModule m, transform::PassContext ctx) {
IRVisitorWithAnalyzer bound_analyzer;
bound_analyzer(func->body);
auto pass = BufferShapeLegalize(func->buffer_map, &bound_analyzer);
auto fptr = func.CopyOnWrite();
fptr->body = pass(std::move(fptr->body));
if (auto map = func->attrs.GetAttr<Map<Buffer, Array<IndexMap>>>("layout_transform_map")) {
func = WithAttr(std::move(func), "layout_transform_map", pass.UpdateIndexMap(map.value()));
}
return func;
};
return transform::CreatePrimFuncPass(pass_func, 0, "tir.BufferShapeLegalize", {});
}
explicit BufferShapeLegalize(const Map<Var, Buffer>& extern_buffer_map,
IRVisitorWithAnalyzer* bound_analyzer)
: bound_analyzer_(bound_analyzer) {
for (auto kv : extern_buffer_map) {
Buffer buf = kv.second;
extern_buffers_.insert(buf);
BufferEntry remap;
remap.remap_to = buf;
remap.index_offsets = Array<PrimExpr>(buf->shape.size(), 0);
remap.in_scope = true;
buf_map_[buf] = remap;
}
}
Map<Buffer, Array<IndexMap>> UpdateIndexMap(const Map<Buffer, Array<IndexMap>>& orig) {
Map<Buffer, Array<IndexMap>> output;
for (const auto& kv : orig) {
auto it = buf_map_.find(kv.first);
if (it != buf_map_.end()) {
output.Set(it->second.remap_to, kv.second);
} else {
output.Set(kv.first, kv.second);
}
}
return output;
}
PrimExpr VisitExpr_(const VarNode* op) final {
auto it = var_remap_.find(op);
if (it != var_remap_.end()) {
return it->second;
} else {
return GetRef<PrimExpr>(op);
}
}
Stmt VisitStmt_(const BufferRealizeNode* op) final {
// BufferRealizeNode for an external buffer serves as an
// annotation of the external buffers, and should not be changed.
// Instead, verify that the bounds match the external
// buffer.
if (extern_buffers_.count(op->buffer)) {
CHECK_EQ(op->buffer->shape.size(), op->bounds.size())
<< "External buffer realize has mismatched dimension";
Stmt stmt = StmtExprMutator::VisitStmt_(op);
op = stmt.as<BufferRealizeNode>();
ICHECK(op);
for (size_t i = 0; i < op->bounds.size(); i++) {
PrimExpr eq = bound_analyzer_->Simplify(op->buffer->shape[i] == op->bounds[i]->extent);
std::ostringstream ss;
ss << "Dim " << i << " of external buffer " << op->buffer->name << " has shape "
<< op->buffer->shape[i] << ", but is only realized for extent " << op->bounds[i]->extent;
if (auto eq_int = eq.as<IntImmNode>()) {
ICHECK(eq_int->value) << ss.str();
} else {
stmt = AssertStmt(eq, tvm::tir::StringImm(ss.str()), stmt);
}
}
return stmt;
}
// Compute the new buffer shape, new realization bounds, and the
// offsets to be applied to buffer access.
Array<PrimExpr> realized_shape;
Array<PrimExpr> index_offsets;
Array<Range> new_bounds;
for (size_t i = 0; i < op->bounds.size(); i++) {
const Range& bound = op->bounds[i];
realized_shape.push_back(bound->extent);
index_offsets.push_back(bound->min);
new_bounds.push_back({0, bound->extent});
}
if (op->buffer->shape.size()) {
ICHECK_EQ(op->buffer->shape.size(), realized_shape.size())
<< "Inconsistency between dimension of buffer " << op->buffer
<< " and dimension of its realized bounds.";
}
Buffer key = op->buffer;
Buffer buf = op->buffer;
auto write_ptr = buf.CopyOnWrite();
write_ptr->shape = realized_shape;
{
BufferEntry remap;
remap.remap_to = buf;
remap.index_offsets = index_offsets;
remap.in_scope = true;
buf_map_[key] = remap;
}
Stmt stmt = BufferRealize(buf, new_bounds, op->condition, this->VisitStmt(op->body), op->span);
buf_map_.at(key).in_scope = false;
return stmt;
}
Stmt VisitStmt_(const BufferStoreNode* op) final {
auto node = Downcast<BufferStore>(StmtExprMutator::VisitStmt_(op));
return VisitBufferAccess(std::move(node));
}
PrimExpr VisitExpr_(const BufferLoadNode* op) final {
auto node = Downcast<BufferLoad>(StmtExprMutator::VisitExpr_(op));
return VisitBufferAccess(std::move(node));
}
template <typename Node>
Node VisitBufferAccess(Node node) {
auto it = buf_map_.find(node->buffer);
if (it != buf_map_.end()) {
const BufferEntry& entry = it->second;
ICHECK(entry.in_scope) << "Cannot access an out-of-scope buffer";
Array<PrimExpr> indices = node->indices;
if (entry.index_offsets.size()) {
ICHECK_GE(entry.index_offsets.size(), indices.size())
<< "Cannot bind buffer to a shape of lower dimension.";
Array<PrimExpr> new_indices;
// Pad leading indices with zero, matching the "fuzzy_match"
// behavior from ArgBinder::BindBuffer.
size_t diff = entry.index_offsets.size() - indices.size();
for (size_t i = 0; i < diff; i++) {
new_indices.push_back(0);
}
// Offset indices used to access buffers of a reduced size.
for (size_t i = 0; i < indices.size(); i++) {
PrimExpr offset = entry.index_offsets[i + diff];
new_indices.push_back(indices[i] - offset);
}
indices = new_indices;
}
auto write_ptr = node.CopyOnWrite();
write_ptr->indices = indices;
write_ptr->buffer = entry.remap_to;
}
return node;
}
Stmt VisitStmt_(const AttrStmtNode* op) final {
if (op->node->IsInstance<tir::BufferNode>()) {
// Visit body before checking internal_buf_map_, because we
// don't know if the BufferNode needs to be changed until we
// look in the body for a BufferRealizeNode with different
// extents.
Stmt body = this->VisitStmt(op->body);
Buffer buffer = Downcast<tir::Buffer>(op->node);
auto it = buf_map_.find(buffer);
if (it != buf_map_.end()) {
buffer = it->second.remap_to;
return AttrStmt(it->second.remap_to, op->attr_key, op->value, body);
}
return AttrStmt(buffer, op->attr_key, op->value, body);
} else if (op->attr_key == attr::buffer_bind_scope) {
return HandleBufferBindScope(op);
}
return StmtExprMutator::VisitStmt_(op);
}
private:
// Any buffers that give views into a resized buffer should be
// updated, both to refer to the resized buffer and to have the view
// window updated. For example, suppose B1 is a 1-D buffer of size
// 100 which is only realized on the range (10,50), and buffer V1 is
// a view into B1[25:35]. When B1 is replaced with B2, a buffer of
// size 40 realized on the range (0,40), V1 must be replaced to be a
// view into B2[15:25].
Stmt HandleBufferBindScope(const AttrStmtNode* op) {
Array<ObjectRef> arr = Downcast<Array<ObjectRef>>(op->node);
ICHECK_EQ(arr.size(), 2U);
Buffer buffer = Downcast<Buffer>(arr[0]);
ICHECK(buffer.defined());
Buffer target = Downcast<Buffer>(arr[1]);
ICHECK(target.defined());
auto it = buf_map_.find(target);
ICHECK(it != buf_map_.end()) << "attr::buffer_bind_scope target " << target << " not in scope.";
const BufferEntry& target_remap = it->second;
ICHECK(target_remap.in_scope) << "Cannot bind " << buffer->name
<< " to the out-of-scope buffer " << target_remap.remap_to->name;
Call tuple = Downcast<Call>(op->value);
ICHECK(tuple.defined() && tuple->op.same_as(builtin::tvm_tuple()));
Array<PrimExpr> new_tuple_args;
Array<PrimExpr> realized_begins;
Array<PrimExpr> view_shape;
ICHECK_EQ(tuple->args.size(), target_remap.index_offsets.size() * 2)
<< "attr::buffer_bind_scope to define " << buffer << " as a view into " << target
<< " does match dimensionality of " << target;
for (size_t i = 0; i < target_remap.index_offsets.size(); i++) {
PrimExpr parent_begin = tuple->args[2 * i];
PrimExpr view_extent = tuple->args[2 * i + 1];
// Offset the begin of the buffer view by the offset of the target buffer.
new_tuple_args.push_back(parent_begin - target_remap.index_offsets[i]);
// Keep the extent of the buffer view the same.
new_tuple_args.push_back(view_extent);
// Use the extent of the buffer view to define the buffer view's shape.
view_shape.push_back(view_extent);
// Within the buffer view, indices start at 0.
realized_begins.push_back(0);
}
// If a view is binding to a buffer of a higher dimensionality,
// then the leading dimensions should be padded out with shape of
// 1.
ICHECK_GE(view_shape.size(), buffer->shape.size())
<< "Cannot bind " << buffer << " to a shape of lower dimension.";
if (view_shape.size() > buffer->shape.size()) {
size_t diff = view_shape.size() - buffer->shape.size();
Array<PrimExpr> padded_shape;
for (size_t i = 0; i < diff; i++) {
padded_shape.push_back(1);
}
for (auto dim : buffer->shape) {
padded_shape.push_back(dim);
}
view_shape = std::move(padded_shape);
}
// If a buffer has strides defined, and is being remapped into a
// shape with additional dimensions, then define dummy values for
// the strides.
Array<PrimExpr> realized_strides = buffer->strides;
if ((realized_strides.size() > 0) && (realized_strides.size() != view_shape.size())) {
ICHECK_GE(view_shape.size(), realized_strides.size())
<< "Cannot bind the strides of " << buffer << " to a shape of lower dimension";
size_t diff = view_shape.size() - buffer->strides.size();
Array<PrimExpr> updated_strides;
for (size_t i = 0; i < diff; i++) {
updated_strides.push_back(Var("stride", buffer->shape[0].dtype()));
}
for (auto stride : buffer->strides) {
updated_strides.push_back(stride);
}
realized_strides = updated_strides;
}
Buffer key = buffer;
auto write_ptr = buffer.CopyOnWrite();
write_ptr->shape = view_shape;
write_ptr->strides = realized_strides;
{
BufferEntry remap;
remap.index_offsets = realized_begins;
remap.remap_to = buffer;
remap.in_scope = true;
buf_map_[key] = remap;
}
// Define remappings of any Variables referencing Buffer internals
// (e.g. Store/Load nodes). Passing fuzzy_match=true allows the
// remapped buffer to have a number of dimensions.
ArgBinder binder(&var_remap_);
binder.BindBuffer(key, buffer, key->name, true);
Stmt body = this->VisitStmt(op->body);
body = MergeNest(binder.asserts(), body);
body = MergeNest(binder.init_nest(), body);
Stmt stmt = AttrStmt(Array<ObjectRef>{buffer, target_remap.remap_to}, op->attr_key,
Call(tuple->dtype, tuple->op, new_tuple_args, tuple->span), body);
for (const Var& v : binder.defs()) {
var_remap_.erase(v.get());
}
buf_map_.at(key).in_scope = false;
return stmt;
}
std::unordered_map<const VarNode*, PrimExpr> var_remap_;
std::unordered_set<Buffer, ObjectPtrHash, ObjectPtrEqual> extern_buffers_;
struct BufferEntry {
Buffer remap_to;
Array<PrimExpr> index_offsets;
bool in_scope;
};
std::unordered_map<Buffer, BufferEntry, ObjectPtrHash, ObjectPtrEqual> buf_map_;
IRVisitorWithAnalyzer* bound_analyzer_;
};
/* Apply dimension alignment restrictions
*
* Buffers annotated with attr::buffer_dim_align may need to have
* strides defined such that they are no longer in a compact shape.
* After this pass, buffers have stride definitions to include these
* alignment restrictions, and attr::buffer_dim_align annotations have
* been removed.
*/
class BufferStrideLegalize : public StmtExprMutator {
public:
static transform::Pass Pass() {
auto pass_func = [](PrimFunc func, IRModule m, transform::PassContext ctx) {
IRVisitorWithAnalyzer bound_analyzer;
bound_analyzer(func->body);
auto pass = BufferStrideLegalize(func->buffer_map, &bound_analyzer);
auto fptr = func.CopyOnWrite();
fptr->body = pass(std::move(fptr->body));
if (auto map = func->attrs.GetAttr<Map<Buffer, Array<IndexMap>>>("layout_transform_map")) {
func = WithAttr(std::move(func), "layout_transform_map", pass.UpdateIndexMap(map.value()));
}
return func;
};
return transform::CreatePrimFuncPass(pass_func, 0, "tir.BufferStrideLegalize", {});
}
explicit BufferStrideLegalize(const Map<Var, Buffer>& extern_buffer_map,
IRVisitorWithAnalyzer* bound_analyzer)
: bound_analyzer_(bound_analyzer) {
for (auto kv : extern_buffer_map) {
Buffer buf = kv.second;
Buffer with_strides = WithStrides(buf);
{
BufferEntry entry;
entry.remap_to = with_strides;
entry.in_scope = true;
entry.is_external = true;
buf_map_[buf] = entry;
}
updated_extern_buffer_map_.Set(kv.first, with_strides);
}
}
Map<Buffer, Array<IndexMap>> UpdateIndexMap(const Map<Buffer, Array<IndexMap>>& orig) {
Map<Buffer, Array<IndexMap>> output;
for (const auto& kv : orig) {
auto it = buf_map_.find(kv.first);
if (it != buf_map_.end()) {
output.Set(it->second.remap_to, kv.second);
} else {
output.Set(kv.first, kv.second);
}
}
return output;
}
Map<Var, Buffer> UpdatedExternBufferMap() const { return updated_extern_buffer_map_; }
Buffer WithStrides(Buffer buf) {
auto it = buf_map_.find(buf);
if (it != buf_map_.end()) {
const BufferEntry& entry = it->second;
ICHECK(entry.in_scope) << "Cannot annotate an out-of-scope buffer";
return entry.remap_to;
}
if (buf->strides.size()) {
ICHECK_EQ(buf->strides.size(), buf->shape.size())
<< "Buffer " << buf << " has inconsistent strides/shape.";
return buf;
}
// Keeping this to have matched behavior to previous version.
// There are many parts of the codebase that assume that a strided
// array cannot be compact. For example, ArgBinder::BindBuffer
// and tir.Specialize.
if (dim_align_.count(buf) == 0) {
return buf;
}
// Can't define the strides for a buffer without a known shape.
Array<PrimExpr> shape = buf->shape;
if (shape.size() == 0) {
return buf;
}
std::vector<PrimExpr> rstrides;
const std::vector<DimAlignInfo>& avec = dim_align_[buf];
int first_dim = 0;
PrimExpr stride = make_const(shape[first_dim].dtype(), 1);
for (size_t i = shape.size(); i != 0; --i) {
size_t dim = i - 1;
if (dim < avec.size() && avec[dim].align_factor != 0) {
PrimExpr factor = make_const(stride.dtype(), avec[dim].align_factor);
PrimExpr offset = make_const(stride.dtype(), avec[dim].align_offset);
stride = stride + indexmod(factor + offset - indexmod(stride, factor), factor);
stride = bound_analyzer_->Simplify(stride);
}
rstrides.push_back(stride);
stride = stride * shape[dim];
}
auto ptr = buf.CopyOnWrite();
ptr->strides = Array<PrimExpr>(rstrides.rbegin(), rstrides.rend());
return buf;
}
Stmt VisitStmt_(const AttrStmtNode* op) final {
if (op->attr_key == attr::buffer_dim_align) {
auto buffer = Downcast<tir::Buffer>(op->node);
const CallNode* tuple = op->value.as<CallNode>();
ICHECK(tuple && tuple->op.same_as(builtin::tvm_tuple()));
auto& vinfo = dim_align_[buffer];
int dim = tuple->args[0].as<IntImmNode>()->value;
if (static_cast<size_t>(dim) >= vinfo.size()) {
vinfo.resize(dim + 1);
}
vinfo[dim].align_factor = tuple->args[1].as<IntImmNode>()->value;
vinfo[dim].align_offset = tuple->args[2].as<IntImmNode>()->value;
return this->VisitStmt(op->body);
} else if (op->attr_key == attr::buffer_bind_scope) {
Array<ObjectRef> arr = Downcast<Array<ObjectRef>>(op->node);
ICHECK_EQ(arr.size(), 2U);
Buffer source = Downcast<Buffer>(arr[0]);
Buffer target_with_strides = WithStrides(Downcast<Buffer>(arr[1]));
Buffer source_with_strides = WithStrides(source);
{
BufferEntry entry;
entry.remap_to = source_with_strides;
entry.in_scope = true;
entry.is_external = false;
buf_map_[source] = entry;
}
Stmt body = this->VisitStmt(op->body);
return AttrStmt(Array<ObjectRef>{source_with_strides, target_with_strides}, op->attr_key,
op->value, body, op->span);
} else {
return StmtExprMutator::VisitStmt_(op);
}
}
// AllocateNodes may be present from tvm.tir.ir_builder. This can
// be simplified in the future by having AllocateNode hold a buffer,
// rather than a buffer_var.
Stmt VisitStmt_(const AllocateNode* op) final {
buffer_var_defines_.insert(op->buffer_var.get());
return StmtExprMutator::VisitStmt_(op);
}
Stmt VisitStmt_(const AllocateConstNode* op) final {
buffer_var_defines_.insert(op->buffer_var.get());
return StmtExprMutator::VisitStmt_(op);
}
Stmt VisitStmt_(const LetStmtNode* op) final {
if (op->var.dtype().is_handle()) {
buffer_var_defines_.insert(op->var.get());
}
return StmtExprMutator::VisitStmt_(op);
}
PrimExpr VisitExpr_(const LetNode* op) final {
if (op->var.dtype().is_handle()) {
buffer_var_defines_.insert(op->var.get());
}
return StmtExprMutator::VisitExpr_(op);
}
Stmt VisitStmt_(const BufferRealizeNode* op) final {
Buffer key = op->buffer;
Buffer with_strides = WithStrides(op->buffer);
{
BufferEntry entry;
entry.remap_to = with_strides;
entry.in_scope = true;
entry.is_external = false;
buf_map_[key] = entry;
}
Stmt stmt = StmtExprMutator::VisitStmt_(op);
buf_map_[key].in_scope = false;
op = stmt.as<BufferRealizeNode>();
ICHECK(op);
return BufferRealize(with_strides, op->bounds, op->condition, op->body, op->span);
}
Stmt VisitStmt_(const BufferStoreNode* op) final {
auto node = Downcast<BufferStore>(StmtExprMutator::VisitStmt_(op));
return VisitBufferAccess(std::move(node));
}
PrimExpr VisitExpr_(const BufferLoadNode* op) final {
auto node = Downcast<BufferLoad>(StmtExprMutator::VisitExpr_(op));
return VisitBufferAccess(std::move(node));
}
template <typename Node>
Node VisitBufferAccess(Node node) {
auto alloc_key = node->buffer->data.get();
if (!buf_map_.count(node->buffer) && buffer_var_defines_.count(alloc_key)) {
BufferEntry entry;
entry.remap_to = WithStrides(node->buffer);
entry.in_scope = true;
entry.is_external = false;
buf_map_[node->buffer] = entry;
}
auto it = buf_map_.find(node->buffer);
ICHECK(it != buf_map_.end()) << "Cannot find allocated buffer for " << node->buffer;
const BufferEntry& e = it->second;
ICHECK(e.in_scope) << "Cannot access a buffer " << node->buffer->name << ", out of scope";
auto writer = node.CopyOnWrite();
writer->buffer = e.remap_to;
return node;
}
private:
Map<Var, Buffer> updated_extern_buffer_map_;
struct DimAlignInfo {
int align_factor{0};
int align_offset{0};
};
// Dimension alignment
std::unordered_map<Buffer, std::vector<DimAlignInfo>, ObjectPtrHash, ObjectPtrEqual> dim_align_;
struct BufferEntry {
Buffer remap_to;
bool in_scope;
bool is_external;
};
std::unordered_map<Buffer, BufferEntry, ObjectPtrHash, ObjectPtrEqual> buf_map_;
// Set of vars that have occurred in an AllocateNode, but haven't
// yet occurred in a BufferLoad/BufferStore.
std::unordered_set<const VarNode*> buffer_var_defines_;
IRVisitorWithAnalyzer* bound_analyzer_;
};
/* Use the scope of IterVar to determine storage scope.
*
* For buffers that do not have an explicit storage scope defined, a
* reasonable storage scope may be defined based on the thread scope
* that contains the buffer's allocation. All other buffers without a
* scope are assigned to global scope.
*/
class ThreadScopePropagate : public StmtExprMutator {
public:
static transform::Pass Pass() {
auto pass_func = [](PrimFunc func, IRModule m, transform::PassContext ctx) {
auto pass = ThreadScopePropagate(func->buffer_map);
auto fptr = func.CopyOnWrite();
fptr->body = pass(std::move(fptr->body));
if (auto map = func->attrs.GetAttr<Map<Buffer, Array<IndexMap>>>("layout_transform_map")) {
func = WithAttr(std::move(func), "layout_transform_map", pass.UpdateIndexMap(map.value()));
}
return func;
};
return transform::CreatePrimFuncPass(pass_func, 0, "tir.ThreadScopePropagate", {});
}
explicit ThreadScopePropagate(const Map<Var, Buffer>& extern_buffer_map) {
// External buffers shouldn't be overwritten, even if they have a
// BufferRealizeNode.
for (auto kv : extern_buffer_map) {
external_buffers_.insert(kv.second);
}
}
Map<Buffer, Array<IndexMap>> UpdateIndexMap(const Map<Buffer, Array<IndexMap>>& orig) {
Map<Buffer, Array<IndexMap>> output;
for (const auto& kv : orig) {
auto it = buf_remap_.find(kv.first->data);
if (it != buf_remap_.end()) {
output.Set(it->second, kv.second);
} else {
output.Set(kv.first, kv.second);
}
}
return output;
}
PrimExpr VisitExpr_(const VarNode* op) final {
auto it = buf_remap_.find(GetRef<Var>(op));
if (it != buf_remap_.end()) {
return it->second->data;
} else {
return GetRef<PrimExpr>(op);
}
}
Stmt VisitStmt_(const AttrStmtNode* op) final {
ICHECK_NE(op->attr_key, attr::buffer_dim_align)
<< "StorageFlattener assumes that all buffers have accurate strides, "
<< "and all buffer_dim_align annotations are removed. "
<< "Please run BufferStrideLegalize first.";
if (op->attr_key == attr::thread_extent) {
IterVar iv = Downcast<IterVar>(op->node);
ThreadScope ts = ThreadScope::Create(iv->thread_tag);
curr_thread_scope_.push_back(ts);
Stmt stmt = StmtExprMutator::VisitStmt_(op);
curr_thread_scope_.pop_back();
return stmt;
} else if (op->attr_key == attr::buffer_bind_scope) {
return HandleBufferBindScope(op);
} else {
return StmtExprMutator::VisitStmt_(op);
}
}
Stmt VisitStmt_(const BufferRealizeNode* op) final {
Var old_var = op->buffer->data;
// Don't remap buffers that already have an explicit scope,
// or external buffers.
std::string str_scope = GetPtrStorageScope(old_var);
if ((str_scope.length() > 0) || external_buffers_.count(op->buffer)) {
return StmtExprMutator::VisitStmt_(op);
}
ICHECK_EQ(buf_remap_.count(old_var), 0)
<< "Buffer var " << op->buffer->data << " appears in multiple BufferRealize nodes";
StorageScope skey;
if (curr_thread_scope_.size() == 0) {
skey.rank = StorageRank::kGlobal;
} else {
skey.rank = runtime::DefaultStorageRank(curr_thread_scope_.back().rank);
}
auto ptr_type = old_var->type_annotation.as<PointerTypeNode>();
ICHECK(ptr_type);
Var new_var(old_var->name_hint, PointerType(ptr_type->element_type, skey.to_string()),
old_var->span);
Buffer buf = op->buffer;
buf.CopyOnWrite()->data = new_var;
buf_remap_[old_var] = buf;
Stmt body = this->VisitStmt(op->body);
return BufferRealize(buf, op->bounds, op->condition, body, op->span);
}
PrimExpr VisitExpr_(const BufferLoadNode* op) final {
PrimExpr expr = StmtExprMutator::VisitExpr_(op);
op = expr.as<BufferLoadNode>();
ICHECK(op);
auto it = buf_remap_.find(op->buffer->data);
if (it != buf_remap_.end()) {
return BufferLoad(it->second, op->indices, op->span);
} else {
return expr;
}
}
Stmt VisitStmt_(const BufferStoreNode* op) final {
Stmt stmt = StmtExprMutator::VisitStmt_(op);
op = stmt.as<BufferStoreNode>();
ICHECK(op);
auto it = buf_remap_.find(op->buffer->data);
if (it != buf_remap_.end()) {
return BufferStore(it->second, op->value, op->indices, op->span);
} else {
return stmt;
}
}
private:
// If the rewritten buffers are part of a buffer_bind_scope, either
// as the buffer view or as the buffer being viewed, then the
// buffer_bind_scope must be rewritten to refer to the updated
// buffers.
Stmt HandleBufferBindScope(const AttrStmtNode* op) {
Array<ObjectRef> arr = Downcast<Array<ObjectRef>>(op->node);
ICHECK_EQ(arr.size(), 2U);
Buffer buffer = Downcast<Buffer>(arr[0]);
ICHECK(buffer.defined());
Buffer target = Downcast<Buffer>(arr[1]);
ICHECK(target.defined());
bool needs_rewrite = false;
{
auto it = buf_remap_.find(buffer->data);
if (it != buf_remap_.end()) {
needs_rewrite = true;
buffer = it->second;
}
}
{
auto it = buf_remap_.find(target->data);
if (it != buf_remap_.end()) {
needs_rewrite = true;
target = it->second;
}
}
if (needs_rewrite) {
Stmt body = this->VisitStmt(op->body);
return AttrStmt(Array<ObjectRef>{buffer, target}, op->attr_key, op->value, body);
} else {
return StmtExprMutator::VisitStmt_(op);
}
}
std::unordered_map<Var, Buffer, ObjectPtrHash, ObjectPtrEqual> buf_remap_;
std::unordered_set<Buffer, ObjectPtrHash, ObjectPtrEqual> external_buffers_;
// The current thread scope.
std::vector<ThreadScope> curr_thread_scope_;
};
/* Map buffer binds to their source buffer
*
* Buffers defined using an attr::buffer_bind_scope annotation are
* views into some linked buffer, potentially into some restricted
* subregion of that buffer. This pass identifies such buffers, then
* rewrites all access of the bound buffers to be access into the
* linked buffer.
*/
class BufferBindUnwrapper : public StmtExprMutator {
public:
static transform::Pass Pass() {
auto pass_func = [](PrimFunc func, IRModule m, transform::PassContext ctx) {
IRVisitorWithAnalyzer bound_analyzer;
bound_analyzer(func->body);
auto pass = BufferBindUnwrapper(func->buffer_map, &bound_analyzer);
auto fptr = func.CopyOnWrite();
fptr->body = pass(std::move(fptr->body));
return func;
};
return transform::CreatePrimFuncPass(pass_func, 0, "tir.BufferBindUnwrapper", {});
}
explicit BufferBindUnwrapper(const Map<Var, Buffer>& extern_buffer_map,
IRVisitorWithAnalyzer* bound_analyzer)
: bound_analyzer_(bound_analyzer) {
for (auto kv : extern_buffer_map) {
BufferEntry e;
e.buffer = kv.second;
e.external = true;
buf_map_[kv.second.get()] = std::move(e);
}
}
Map<Buffer, Array<IndexMap>> UpdateIndexMap(const Map<Buffer, Array<IndexMap>>& orig) {
Map<Buffer, Array<IndexMap>> output;
for (const auto& kv : orig) {
const BufferEntry& e = GetBufferEntry(kv.first);
if (e.remap) {
output.Set(e.remap->target, kv.second);
} else {
output.Set(kv.first, kv.second);
}
}
return output;
}
Stmt VisitStmt_(const StoreNode* op) final {
LOG(FATAL) << "Unexpected use of deprecated StoreNode. Please use BufferStoreNode instead.";
return Stmt();
}
PrimExpr VisitExpr_(const LoadNode* op) final {
LOG(FATAL) << "Unexpected use of deprecated LoadNode. Please use BufferLoadNode instead.";
return PrimExpr();
}
Stmt VisitStmt_(const AttrStmtNode* op) final {
ICHECK_NE(op->attr_key, attr::buffer_dim_align)
<< "BufferBindUnwrapper assumes that all buffers have accurate strides, "
<< "and all buffer_dim_align annotations are removed. "
<< "Please run BufferStrideLegalize first.";
if (op->attr_key == attr::buffer_bind_scope) {
return HandleBufferBindScope(op);
} else {
return StmtExprMutator::VisitStmt_(op);
}
}
PrimExpr VisitExpr_(const VarNode* op) final {
ICHECK(!illegal_vars_.count(op)) << "Variable " << op->name_hint << " is not well defined. "
<< "(e.g. use of buffer.elem_offset for a non-flat buffer)";
auto it = var_remap_.find(op);
if (it != var_remap_.end()) {
return it->second;
} else {
return GetRef<PrimExpr>(op);
}
}
Array<PrimExpr> remap_indices(Array<PrimExpr> indices, Array<PrimExpr> begins,
Array<PrimExpr> extents) {
ICHECK_EQ(begins.size(), extents.size());
if (begins.size() == 0) {
return indices;
}
ICHECK_EQ(begins.size(), indices.size());
Array<PrimExpr> out;
for (size_t i = 0; i < begins.size(); i++) {
out.push_back(begins[i] + indices[i]);
}
return out;
}
Array<Range> remap_bounds(Array<Range> bounds, Array<PrimExpr> begins, Array<PrimExpr> extents) {
ICHECK_EQ(begins.size(), extents.size());
if (begins.size() == 0) {
return bounds;
}
ICHECK_EQ(begins.size(), bounds.size());
Array<Range> out;
for (size_t i = 0; i < begins.size(); i++) {
out.push_back(Range::FromMinExtent(bounds[i]->min + begins[i], bounds[i]->extent));
}
return out;
}
// AllocateNodes may be present from tvm.tir.ir_builder. This can
// be simplified in the future by having AllocateNode hold a buffer,
// rather than a buffer_var.
Stmt VisitStmt_(const AllocateNode* op) final {
buffer_var_defines_.insert(op->buffer_var.get());
return StmtExprMutator::VisitStmt_(op);
}
Stmt VisitStmt_(const AllocateConstNode* op) final {
buffer_var_defines_.insert(op->buffer_var.get());
return StmtExprMutator::VisitStmt_(op);
}
Stmt VisitStmt_(const LetStmtNode* op) final {
if (op->var.dtype().is_handle()) {
buffer_var_defines_.insert(op->var.get());
}
return StmtExprMutator::VisitStmt_(op);
}
PrimExpr VisitExpr_(const LetNode* op) final {
if (op->var.dtype().is_handle()) {
buffer_var_defines_.insert(op->var.get());
}
return StmtExprMutator::VisitExpr_(op);
}
PrimExpr VisitExpr_(const BufferLoadNode* op) final {
PrimExpr expr = StmtExprMutator::VisitExpr_(op);
op = expr.as<BufferLoadNode>();
const BufferEntry& e = GetBufferEntry(op->buffer);
if (e.remap) {
return BufferLoad(e.remap->target,
remap_indices(op->indices, e.remap->begins, e.remap->extents), op->span);
} else {
return expr;
}
}
Stmt VisitStmt_(const BufferStoreNode* op) final {
Stmt stmt = StmtExprMutator::VisitStmt_(op);
op = stmt.as<BufferStoreNode>();
const BufferEntry& e = GetBufferEntry(op->buffer);
if (e.remap) {
return BufferStore(e.remap->target, op->value,
remap_indices(op->indices, e.remap->begins, e.remap->extents), op->span);
} else {
return stmt;
}
}
Stmt VisitStmt_(const BufferRealizeNode* op) final {
const auto& key = op->buffer.get();
bool is_external = false;
if (buf_map_.count(key)) {
ICHECK(buf_map_.at(key).external)
<< "BufferRealize node for internal buffer " << op->buffer << " occurred multiple times.";
is_external = true;
} else {
BufferEntry e;
e.bounds = op->bounds;
e.buffer = op->buffer;
buf_map_[key] = std::move(e);
}
Stmt stmt = StmtExprMutator::VisitStmt_(op);
if (is_external) {
buf_map_[key].in_scope = false;
}
return stmt;
}
Stmt VisitStmt_(const PrefetchNode* op) final {
Stmt stmt = StmtExprMutator::VisitStmt_(op);
op = stmt.as<PrefetchNode>();
ICHECK(op != nullptr);
const BufferEntry& e = GetBufferEntry(op->buffer);
ICHECK(e.in_scope) << "Read a buffer that is already out of scope";
ICHECK_EQ(e.buffer->shape.size(), op->bounds.size())
<< "Prefetch dim should be the same as buffer dim";
if (e.remap) {
return Prefetch(e.remap->target, remap_bounds(op->bounds, e.remap->begins, e.remap->extents),
op->span);
} else {
return stmt;
}
}
private:
// Read the mapping from a buffer view to the actual buffer. This
// allows all later BufferStore/BufferLoad nodes to reference the
// actual buffer, rather than the buffer view.
Stmt HandleBufferBindScope(const AttrStmtNode* op) {
// Unpack information from Attribute node
RemapInfo remap;
Array<ObjectRef> arr = Downcast<Array<ObjectRef>>(op->node);
ICHECK_EQ(arr.size(), 2U);
const Buffer source = Downcast<Buffer>(arr[0]);
ICHECK(source.defined());
remap.target = Downcast<Buffer>(arr[1]);
ICHECK(remap.target.defined());
const CallNode* tuple = op->value.as<CallNode>();
ICHECK(tuple && tuple->op.same_as(builtin::tvm_tuple()));
for (size_t i = 0; i < tuple->args.size(); i += 2) {
remap.begins.push_back(tuple->args[i]);
remap.extents.push_back(tuple->args[i + 1]);
}
// Determine bounds in the target buffer
auto it = buf_map_.find(remap.target.get());
ICHECK(it != buf_map_.end()) << "Cannot define " << source << " as a view into " << remap.target
<< ", " << remap.target << " was not defined.";
const BufferEntry& target_info = it->second;
ICHECK(target_info.in_scope) << "Cannot define " << source << " as a view into " << remap.target
<< ", " << remap.target << " is out of scope.";
ICHECK_EQ(remap.begins.size(), target_info.buffer->shape.size())
<< "Incorrect number of arguments in buffer_bind_scope attribute. "
<< "Expected (min_0, extent_0, min_1, extent_0, ..., min_N, extent_N).";
if (target_info.bounds.size() > 0) {
Array<PrimExpr> mapped_begins;
for (size_t i = 0; i < target_info.buffer->shape.size(); ++i) {
mapped_begins.push_back(remap.begins[i] - target_info.bounds[i]->min);
}
remap.begins = std::move(mapped_begins);
}
ICHECK(target_info.remap == nullptr)
<< "buffer_bind_scope defines " << source << " as a view into " << remap.target
<< ", which is itself a buffer view. "
<< "Indirect remapping not currently supported.";
for (size_t i = 0; i < remap.begins.size(); i++) {
remap.begins.Set(i, bound_analyzer_->Simplify(remap.begins[i]));
remap.extents.Set(i, bound_analyzer_->Simplify(remap.extents[i]));
}
// Add a buffer remap entry
{
BufferEntry source_info;
source_info.buffer = source;
source_info.remap = std::make_unique<RemapInfo>(remap);
buf_map_[source.get()] = std::move(source_info);
}
// Define remappings of any remaining Variables (e.g. Store/Load nodes).
ArgBinder binder(&var_remap_);
// Define a view that represents the source's view into the target
// buffer. This Buffer object is only used to define the mapping
// to the target buffer, and never actually appears in the TIR
// graph.
Buffer view = remap.target.MakeSlice(remap.begins, remap.extents);
if (source->strides.size() == 0) {
ICHECK_EQ(view->strides.size(), 0U)
<< "Cannot bind a compact buffer " << source << " to a strided buffer " << view
<< " with strides " << view->strides;
} else {
// Add explicit strides to the view, in order to bind to source.strides[i].
view = view.MakeStrideView();
}
// Match integer bits of source->elem_offset and view->elem_offset
// as is required by ArgBinder::Bind_
if (view->elem_offset.defined() && source->elem_offset.dtype() != view->elem_offset.dtype()) {
view.CopyOnWrite()->elem_offset = cast(source->elem_offset.dtype(), view->elem_offset);
}
// Bind any variables that reference the view (e.g. elem_offset,
// strides, shape). Pass fuzzy_match=false, because all shape
// transformations should have been handled in
// BufferShapeLegalize.
binder.BindBuffer(source, view, source->name, false);
if (auto* elem_offset_var = source->elem_offset.as<VarNode>()) {
if (!view->elem_offset.defined()) {
illegal_vars_.insert(elem_offset_var);
}
}
// Apply the remaps
Stmt body = op->body;
body = MergeNest(binder.asserts(), body);
body = MergeNest(binder.init_nest(), body);
body = this->VisitStmt(body);
// remove the binds
for (const Var& v : binder.defs()) {
var_remap_.erase(v.get());
}
return body;
}
struct RemapInfo {
Buffer target;
Array<PrimExpr> begins;
Array<PrimExpr> extents;
};
// The buffer entry in the flatten map
struct BufferEntry {
// The storage buffer
Buffer buffer;
// the bounds of realization, can be null, means everything
Region bounds;
// Whether the buffer is external
bool external{false};
// Whether we are within the allocation scope of the buffer.
bool in_scope{true};
// The buffer to which the storage buffer should be remapped.
std::unique_ptr<RemapInfo> remap{nullptr};
};
const BufferEntry& GetBufferEntry(Buffer buffer) {
auto alloc_key = buffer->data.get();
if (!buf_map_.count(buffer.get()) && buffer_var_defines_.count(alloc_key)) {
BufferEntry entry;
entry.buffer = buffer;
buf_map_[buffer.get()] = std::move(entry);
}
auto it = buf_map_.find(buffer.get());
ICHECK(it != buf_map_.end()) << "Cannot find allocated buffer for " << buffer;
const BufferEntry& e = it->second;
ICHECK(e.in_scope) << "Cannot access a buffer " << buffer->name << ", out of scope";
return it->second;
}
// The buffer assignment map
// Variable remap
std::unordered_map<const VarNode*, PrimExpr> var_remap_;
// Variables that may not occur within the body.
std::unordered_set<const VarNode*> illegal_vars_;
// Buffer map
std::unordered_map<const BufferNode*, BufferEntry> buf_map_;
// Set of vars that have occurred in an AllocateNode, but haven't
// yet occurred in a BufferLoad/BufferStore.
std::unordered_set<const VarNode*> buffer_var_defines_;
// Analyzer for the variable bounds, used to simplify the bounds populator. We really need the
// analyzer from it. However
IRVisitorWithAnalyzer* bound_analyzer_;
};
class ApplyLayoutTransforms : public StmtExprMutator {
public:
static transform::Pass Pass() {
auto pass_func = [](PrimFunc func, IRModule m, transform::PassContext ctx) {
auto lookup = func->attrs.GetAttr<Map<Buffer, Array<IndexMap>>>("layout_transform_map");
if (!lookup) {
return func;
}
Map<Buffer, Array<IndexMap>> layout_transforms = lookup.value();
auto fptr = func.CopyOnWrite();
auto mutator = ApplyLayoutTransforms(layout_transforms);
fptr->buffer_map = mutator.UpdateExternBufferMap(fptr->buffer_map);
fptr->body = mutator(std::move(fptr->body));
return WithoutAttr(std::move(func), "layout_transform_map");
};
return transform::CreatePrimFuncPass(pass_func, 0, "tir.ApplyLayoutTransforms", {});
}
explicit ApplyLayoutTransforms(Map<Buffer, Array<IndexMap>> layout_transforms)
: layout_transforms_(layout_transforms) {}
Map<tir::Var, Buffer> UpdateExternBufferMap(const Map<tir::Var, Buffer>& buffer_map) {
Map<tir::Var, Buffer> output;
for (const auto& kv : buffer_map) {
output.Set(kv.first, GetBufferRemap(kv.second, true));
}
return output;
}
Stmt VisitStmt_(const BufferRealizeNode* op) final {
// Call once so that load/store nodes can read from the cached
// value.
GetBufferRemap(op->buffer, true);
auto realize = Downcast<BufferRealize>(StmtExprMutator::VisitStmt_(op));
auto lookup = layout_transforms_.Get(op->buffer);
if (lookup) {
auto write_ptr = realize.CopyOnWrite();
write_ptr->buffer = GetBufferRemap(op->buffer, true);
Array<IndexMap> transforms = lookup.value();
for (const auto& transform : transforms) {
write_ptr->bounds = transform->MapRanges(realize->bounds);
}
}
return std::move(realize);
}
Stmt VisitStmt_(const BufferStoreNode* op) final {
auto node = Downcast<BufferStore>(StmtExprMutator::VisitStmt_(op));
return VisitBufferAccess(std::move(node));
}
PrimExpr VisitExpr_(const BufferLoadNode* op) final {
auto node = Downcast<BufferLoad>(StmtExprMutator::VisitExpr_(op));
return VisitBufferAccess(std::move(node));
}
template <typename Node>
Node VisitBufferAccess(Node node) {
auto lookup = layout_transforms_.Get(node->buffer);
if (lookup) {
auto write_ptr = node.CopyOnWrite();
write_ptr->buffer = GetBufferRemap(node->buffer);
Array<IndexMap> transforms = lookup.value();
for (const auto& transform : transforms) {
write_ptr->indices = transform->MapIndices(node->indices);
}
}
return node;
}
private:
//! \brief Given a buffer, return the buffer it should be remapped into.
Buffer GetBufferRemap(Buffer buf, bool allow_alloc = false) {
auto key = buf.get();
auto it = buf_map_.find(key);
if (it != buf_map_.end()) {
return it->second;
}
ICHECK(allow_alloc) << "Buffer " << buf << " accessed before declaration.";
auto lookup = layout_transforms_.Get(buf);
if (lookup) {
Array<IndexMap> transforms = lookup.value();
auto write_ptr = buf.CopyOnWrite();
for (const auto& transform : transforms) {
write_ptr->shape = transform->MapShape(buf->shape);
}
}
buf_map_[key] = buf;
return buf;
}
std::unordered_map<const BufferNode*, Buffer> buf_map_;
Map<Buffer, Array<IndexMap>> layout_transforms_;
};
class StorageFlattener : public StmtExprMutator {
public:
static transform::Pass Pass(int cache_line_size, bool create_bound_attributes) {
auto pass_func = [=](PrimFunc func, IRModule m, transform::PassContext ctx) {
IRVisitorWithAnalyzer bound_analyzer;
bound_analyzer(func->body);
auto pass = StorageFlattener(func->buffer_map, cache_line_size, create_bound_attributes,
&bound_analyzer);
Map<Var, Buffer> preflattened_buffer_map =
Merge(func->buffer_map, func->preflattened_buffer_map);
auto fptr = func.CopyOnWrite();
fptr->body = pass(std::move(fptr->body));
fptr->preflattened_buffer_map = preflattened_buffer_map;
fptr->buffer_map = pass.UpdatedBufferMap();
return func;
};
return transform::CreatePrimFuncPass(pass_func, 0, "tir.StorageFlattener", {});
}
explicit StorageFlattener(const Map<Var, Buffer>& extern_buffer_map, int cache_line_size,
bool create_bound_attributes, IRVisitorWithAnalyzer* bound_analyzer)
: bound_analyzer_(bound_analyzer), create_bound_attributes_(create_bound_attributes) {
for (auto kv : extern_buffer_map) {
BufferEntry e;
e.buffer = kv.second;
e.flattened_buffer = e.buffer.GetFlattenedBuffer();
// TODO(Lunderberg): Move the handling of boolean into a
// dedicated pass.
// Boolean tensors are backed by a Int8 array.
if (e.buffer->dtype == DataType::Bool()) {
{
auto writer = e.buffer.CopyOnWrite();
writer->dtype = DataType::Int(8);
}
{
auto writer = e.flattened_buffer.CopyOnWrite();
writer->dtype = DataType::Int(8);
}
}
e.external = true;
buf_map_[kv.second] = e;
updated_extern_buffer_map_.Set(kv.first, e.flattened_buffer);
}
cache_line_size_ = cache_line_size;
}
Map<Var, Buffer> UpdatedBufferMap() { return updated_extern_buffer_map_; }
Stmt VisitStmt_(const StoreNode* op) final {
LOG(FATAL) << "Unexpected use of deprecated StoreNode. Please use BufferStoreNode instead.";
return Stmt();
}
PrimExpr VisitExpr_(const LoadNode* op) final {
LOG(FATAL) << "Unexpected use of deprecated LoadNode. Please use BufferLoadNode instead.";
return PrimExpr();
}
Stmt VisitStmt_(const AttrStmtNode* op) final {
ICHECK_NE(op->attr_key, attr::buffer_dim_align)
<< "StorageFlattener assumes that all buffers have accurate strides, "
<< "and all buffer_dim_align annotations are removed. "
<< "Please run BufferStrideLegalize first.";
ICHECK_NE(op->attr_key, attr::buffer_bind_scope)
<< "StorageFlattener assumes that all buffer binds have already been applied. "
<< "Please run BufferBindUnwrapper first.";
if (op->attr_key == attr::double_buffer_scope && op->node->IsInstance<tir::BufferNode>()) {
auto buffer = Downcast<tir::Buffer>(op->node);
Stmt body = this->VisitStmt(op->body);
const auto& entry = GetBufferEntry(buffer);
body = AttrStmt(entry.flattened_buffer->data, op->attr_key, op->value, std::move(body));
return body;
}
return StmtExprMutator::VisitStmt_(op);
}
Stmt VisitStmt_(const BufferStoreNode* op) final {
if (create_bound_attributes_) shape_collector_.clear();
Stmt stmt = StmtExprMutator::VisitStmt_(op);
op = stmt.as<BufferStoreNode>();
const BufferEntry& e = GetBufferEntry(op->buffer);
// Handle casts from the value's dtype to the dtype of the backing
// array.
PrimExpr value = op->value;
if (value.dtype() == DataType::Bool()) {
ICHECK_EQ(e.flattened_buffer->dtype, DataType::Int(8))
<< "Expected int8 backing array for boolean tensor, but received "
<< e.flattened_buffer->dtype;
value = tir::Cast(DataType::Int(8), value);
}
auto flattened_indices = e.buffer->ElemOffset(op->indices);
Stmt body = BufferStore(e.flattened_buffer, value, flattened_indices, op->span);
if (create_bound_attributes_ && ShapeIsValid(e.buffer->shape)) {
shape_collector_.push_back(std::make_pair(e.buffer->data, e.buffer->shape));
}
// To create bound attribute collector should has at least one item.
if (create_bound_attributes_ && shape_collector_.size()) {
for (size_t i = 0; i < shape_collector_.size(); ++i) {
body = AttrStmt(shape_collector_[i].first, tir::attr::buffer_bound,
MakeBound(e.buffer->dtype, shape_collector_[i].second), body);
}
}
return body;
}
// AllocateNodes may be present from tvm.tir.ir_builder. This can
// be simplified in the future by having AllocateNode hold a buffer,
// rather than a buffer_var.
Stmt VisitStmt_(const AllocateNode* op) final {
buffer_var_defines_.insert(op->buffer_var.get());
auto stmt = Downcast<Allocate>(StmtExprMutator::VisitStmt_(op));
return Allocate(stmt->buffer_var, stmt->dtype, FlattenExtents(stmt), stmt->condition,
stmt->body, stmt->annotations, stmt->span);
}
Stmt VisitStmt_(const AllocateConstNode* op) final {
buffer_var_defines_.insert(op->buffer_var.get());
auto stmt = Downcast<AllocateConst>(StmtExprMutator::VisitStmt_(op));
ObjectRef data_or_idx;
if (stmt->data) {
data_or_idx = stmt->data.value();
} else if (stmt->irmod_storage_idx) {
data_or_idx = stmt->irmod_storage_idx.value();
} else {
LOG(FATAL) << "Neither data array nor data index specified for allocation of const "
<< op->buffer_var->name_hint;
}
return AllocateConst(stmt->buffer_var, stmt->dtype, FlattenExtents(stmt), data_or_idx,
stmt->body, stmt->annotations, stmt->span);
}
Stmt VisitStmt_(const LetStmtNode* op) final {
if (op->var.dtype().is_handle()) {
buffer_var_defines_.insert(op->var.get());
}
return StmtExprMutator::VisitStmt_(op);
}
PrimExpr VisitExpr_(const LetNode* op) final {
if (op->var.dtype().is_handle()) {
buffer_var_defines_.insert(op->var.get());
}
return StmtExprMutator::VisitExpr_(op);
}
Stmt VisitStmt_(const BufferRealizeNode* op) final {
const auto& key = op->buffer;
if (buf_map_.count(key)) {
ICHECK(buf_map_.at(key).external)
<< "BufferRealize for internal buffer " << op->buffer << " appears multiple times.";
return this->VisitStmt(op->body);
} else {
// create a buffer entry
BufferEntry e;
ICHECK_EQ(op->buffer->shape.size(), op->bounds.size())
<< "Inconsistent buffer shape and realization shape for " << op->buffer;
for (size_t i = 0; i < op->bounds.size(); i++) {
const auto& bound = op->bounds[i];
const auto& dim_size = op->buffer->shape[i];
ICHECK(is_zero(bound_analyzer_->Simplify(bound->min)))
<< "Buffer " << op->buffer << " has realization bounds that do not start at zero. "
<< "Please run BufferShapeLegalize first.";
ICHECK(is_one(bound_analyzer_->Simplify(bound->extent == dim_size)))
<< "Buffer " << op->buffer
<< " has realization extent that does not match its size. "
"Please run BufferShapeLegalize first.";
}
StorageScope skey = StorageScope::Create(GetPtrStorageScope(op->buffer->data));
// use small alignment for small arrays
auto dtype = op->buffer->dtype;
size_t const_size = AllocateNode::ConstantAllocationSize(op->buffer->shape);
int align = GetTempAllocaAlignment(dtype, const_size);
if (skey.tag.length() != 0) {
MemoryInfo info = GetMemoryInfo(skey.to_string());
if (info.defined()) {
align = (info->max_simd_bits + dtype.bits() - 1) / dtype.bits();
ICHECK_LE(const_size * dtype.bits(), info->max_num_bits)
<< "Allocation exceed bound of memory tag " << skey.to_string();
}
}
e.buffer = Buffer(op->buffer->data, op->buffer->dtype, op->buffer->shape, op->buffer->strides,
PrimExpr(), op->buffer->name, align, 0, kDefault,
op->buffer->axis_separators, op->buffer->span);
e.flattened_buffer = e.buffer.GetFlattenedBuffer();
// TODO(Lunderberg): Move the handling of boolean into a
// dedicated pass.
// Boolean tensors are backed by a Int8 array.
if (e.flattened_buffer->dtype == DataType::Bool()) {
auto writer = e.flattened_buffer.CopyOnWrite();
writer->dtype = DataType::Int(8);
}
buf_map_[key] = e;
Stmt body = this->VisitStmt(op->body);
buf_map_[key].in_scope = false;
Stmt ret =
Allocate(e.flattened_buffer->data, e.flattened_buffer->dtype, e.flattened_buffer->shape,
make_const(DataType::Bool(e.flattened_buffer->dtype.lanes()), true), body);
if (create_bound_attributes_ && ShapeIsValid(e.buffer->shape)) {
ret = AttrStmt(e.buffer->data, tir::attr::buffer_bound,
MakeBound(e.buffer->dtype, e.buffer->shape), ret);
}
return ret;
}
}
PrimExpr VisitExpr_(const VarNode* op) final {
auto it = var_remap_.find(op);
if (it != var_remap_.end()) {
return it->second;
} else {
return GetRef<PrimExpr>(op);
}
}
PrimExpr VisitExpr_(const BufferLoadNode* op) final {
PrimExpr expr = StmtExprMutator::VisitExpr_(op);
op = expr.as<BufferLoadNode>();
const BufferEntry& e = GetBufferEntry(op->buffer);
if (create_bound_attributes_ && ShapeIsValid(e.buffer->shape)) {
shape_collector_.push_back(std::make_pair(e.buffer->data, e.buffer->shape));
}
auto flattened_indices = e.buffer->ElemOffset(op->indices);
PrimExpr val = BufferLoad(e.flattened_buffer, flattened_indices, op->span);
if (op->dtype == DataType::Bool()) {
ICHECK_EQ(e.flattened_buffer->dtype, DataType::Int(8))
<< "Expected int8 backing array for boolean tensor, but received "
<< e.flattened_buffer->dtype;
val = tir::Cast(DataType::Bool(), val);
}
return val;
}
Stmt VisitStmt_(const PrefetchNode* op) final {
const BufferEntry& e = GetBufferEntry(op->buffer);
ICHECK(e.in_scope) << "Cannot prefetch " << op->buffer << ", out of scope.";
ICHECK_EQ(e.buffer->shape.size(), op->bounds.size())
<< "Prefetch dim should be the same as buffer dim";
int block_size = 1, elem_cnt = cache_line_size_ / e.buffer->dtype.bytes();
int starts = op->bounds.size() - 1;
while (starts > 0) {
auto* shape_as_int = e.buffer->shape[starts].as<IntImmNode>();
if (shape_as_int == nullptr || block_size * shape_as_int->value > elem_cnt) break;
block_size *= static_cast<int>(shape_as_int->value);
starts--;
}
PrimExpr stride(elem_cnt / block_size);
Array<PrimExpr> args;
std::vector<Var> vars;
for (int i = op->bounds.size() - 1; i > starts; --i) {
args.push_back(op->bounds[i]->min);
}
auto& func_name = op->buffer->name;
vars.push_back(Var("prefetch." + func_name + "." + std::to_string(starts), DataType::Int(32)));
args.push_back(op->bounds[starts]->min + stride * vars.back());
for (int i = starts - 1; i >= 0; --i) {
vars.push_back(Var("prefetch." + func_name + "." + std::to_string(i), DataType::Int(32)));
args.push_back(vars.back() + op->bounds[i]->min);
}
Stmt stmt = GetRef<Stmt>(op);
for (int i = starts; i >= 0; --i) {
if (i < starts) {
stmt = For(vars[i], 0, op->bounds[i]->extent, ForKind::kSerial, stmt);
} else {
PrimExpr load = e.buffer.vload(args, e.buffer->dtype);
PrimExpr address = Call(DataType::Handle(), builtin::address_of(), {load});
PrimExpr prefetch = Call(op->buffer->dtype, builtin::prefetch(), {address, 0, 3, 1});
stmt = Evaluate(prefetch);
PrimExpr extent = (op->bounds[i]->extent - 1) / stride + 1;
stmt = For(vars[i], 0, extent, ForKind::kSerial, stmt);
}
}
return this->VisitStmt(stmt);
}
PrimExpr VisitExpr_(const ProducerLoadNode* op) final {
LOG(FATAL) << "ProducerLoad cannot appear in a valid TIR PrimFunc. "
<< "Please run SchedulePostProcToPrimFunc first.";
return PrimExpr();
}
Stmt VisitStmt_(const ProducerStoreNode* op) final {
LOG(FATAL) << "ProducerStore cannot appear in a valid TIR PrimFunc. "
<< "Please run SchedulePostProcToPrimFunc first.";
return Stmt();
}
Stmt VisitStmt_(const ProducerRealizeNode* op) final {
LOG(FATAL) << "ProducerRealize cannot appear in a valid TIR PrimFunc. "
<< "Please run SchedulePostProcToPrimFunc first.";
return Stmt();
}
private:
// Helper function for visiting Allocate and AllocateConst. If, in
// the future, these are updated to hold a buffer (Buffer) object
// rather than a buffer_var (Var), this function can be replaced
// with a call to GetBufferEntry.
template <typename Node>
Array<PrimExpr> FlattenExtents(const Node& node) {
arith::Analyzer analyzer;
// If an allocation has extents that match the buffer
auto is_compatible_buffer = [&](const Buffer& buffer) {
if (buffer->shape.size() != node->extents.size()) {
return false;
}
for (size_t i = 0; i < buffer->shape.size(); i++) {
if (!analyzer.CanProveEqual(buffer->shape[i], node->extents[i])) {
return false;
}
}
return true;
};
auto int_array_equal = [](const Array<IntImm>& a, const Array<IntImm>& b) {
if (a.size() != b.size()) {
return false;
}
for (size_t i = 0; i < a.size(); i++) {
if (a[i]->value != b[i]->value) {
return false;
}
}
return true;
};
Array<IntImm> axis_separators;
auto it = buffer_var_map_.find(node->buffer_var.get());
if (it != buffer_var_map_.end()) {
const auto& buffers = it->second;
if (buffers.size() == 0) {
// No buffers use this allocation, treat as flat and optimize
// out later.
} else if (buffers.size() == 1) {
// Only one buffer uses this allocation, so use its axis
// separators.
axis_separators = buffers[0]->axis_separators;
} else {
// Try to find a buffer using this allocation with a matching
// shape.
Buffer compatible_buffer;
for (const auto& buffer : buffers) {
if (is_compatible_buffer(buffer)) {
ICHECK(!compatible_buffer.defined() ||
int_array_equal(compatible_buffer->axis_separators, buffer->axis_separators))
<< "Cannot determine axis separators to use when flattening "
<< node->buffer_var->name_hint
<< ", multiple buffer objects found with conflicting axis separators";
compatible_buffer = buffer;
}
}
ICHECK(compatible_buffer.defined())
<< "Cannot determine axis separators to use when flattening "
<< node->buffer_var->name_hint << ", no buffers found with matching shape";
axis_separators = compatible_buffer->axis_separators;
}
}
// Use GetFlattenedBuffer to determine the flattened shape of the
// output. We only need the shape and axis separators defined,
// everything else can be dummy values.
Buffer dummy_buffer =
decl_buffer(node->extents, DataType::Float(32), "buffer", "", axis_separators);
return dummy_buffer.GetFlattenedBuffer()->shape;
}
// The buffer entry in the flatten map
struct DimAlignInfo {
int align_factor{0};
int align_offset{0};
};
// The buffer entry in the flatten map
struct BufferEntry {
// The buffer object
Buffer buffer;
// The updated buffer object, after flattening has been applied.
Buffer flattened_buffer;
// Whether the buffer is external
bool external{false};
// Whether the buffer is currently in scope.
bool in_scope{true};
};
bool ShapeIsValid(const Array<PrimExpr>& shape) {
// Zero-dimensional tensor does not need boundary check.
if (!shape.size()) return false;
for (size_t i = 0; i < shape.size(); ++i) {
if (!shape[i].defined() || !shape[i].dtype().is_scalar() || is_negative_const(shape[i])) {
return false;
}
}
return true;
}
PrimExpr MakeBound(const DataType& type, const Array<PrimExpr>& shape) {
// We have already checked the shape size to be greater then 0.
PrimExpr bound = Mul(make_const(shape[0].dtype(), type.lanes()), shape[0]);
for (size_t i = 1; i < shape.size(); ++i) {
bound = Mul(bound, Mul(make_const(bound.dtype(), type.lanes()), shape[i]));
}
Array<PrimExpr> bounds{bound};
return Call(DataType::Handle(), builtin::tvm_tuple(), bounds);
}
const BufferEntry& GetBufferEntry(Buffer buffer) {
auto alloc_key = buffer->data.get();
if (!buf_map_.count(buffer) && buffer_var_defines_.count(alloc_key)) {
BufferEntry entry;
entry.buffer = buffer;
entry.flattened_buffer = buffer.GetFlattenedBuffer();
// Boolean tensors are backed by a Int8 array.
if (entry.flattened_buffer->dtype == DataType::Bool()) {
auto writer = entry.flattened_buffer.CopyOnWrite();
writer->dtype = DataType::Int(8);
}
buf_map_[buffer] = std::move(entry);
}
auto it = buf_map_.find(buffer);
ICHECK(it != buf_map_.end()) << "Cannot find allocated buffer for " << buffer;
const BufferEntry& e = it->second;
ICHECK(e.in_scope) << "Cannot access a buffer " << buffer->name << ", out of scope";
return it->second;
}
// The buffer assignment map
// Variable remap
std::unordered_map<const VarNode*, PrimExpr> var_remap_;
// Set of vars that have occurred in an AllocateNode, but haven't
// yet occurred in a BufferLoad/BufferStore.
std::unordered_set<const VarNode*> buffer_var_defines_;
// Map from an allocation variable to the buffer(s) that it backs.
// Used to track the determine the axis_separators that should be
// used for flattening the extents of an AllocateNode.
std::unordered_map<const VarNode*, std::vector<Buffer>> buffer_var_map_;
// Buffer map
std::unordered_map<Buffer, BufferEntry, ObjectPtrHash, ObjectPtrEqual> buf_map_;
// The extern buffer map, updated to include flattened buffers.
Map<Var, Buffer> updated_extern_buffer_map_;
// Collects shapes.
std::vector<std::pair<Var, Array<PrimExpr>>> shape_collector_;
// bounds populator. We really need the analyzer from it.
// However
IRVisitorWithAnalyzer* bound_analyzer_;
// The size of cacheline
int cache_line_size_;
// Whether to mark load/store with theirs bounds.
bool create_bound_attributes_{false};
};
/*!
* \brief Simplify assert statements.
*
* If an assert statement can be statically verified to be true,
* remove the assert statement. Otherwise, keep the assert statement
* unmodified.
*/
class AssertSimplifier : public StmtMutator {
public:
static transform::Pass Pass() {
auto pass_func = [=](PrimFunc func, IRModule m, transform::PassContext ctx) {
IRVisitorWithAnalyzer bound_analyzer;
bound_analyzer(func->body);
auto fptr = func.CopyOnWrite();
fptr->body = AssertSimplifier(&bound_analyzer)(std::move(fptr->body));
return func;
};
return transform::CreatePrimFuncPass(pass_func, 0, "tir.AssertSimplifier", {});
}
explicit AssertSimplifier(IRVisitorWithAnalyzer* bound_analyzer)
: bound_analyzer_(bound_analyzer) {}
Stmt VisitStmt_(const AssertStmtNode* op) final {
Stmt stmt = StmtMutator::VisitStmt_(op);
op = stmt.as<AssertStmtNode>();
PrimExpr condition = bound_analyzer_->Simplify(op->condition);
if (is_one(condition)) {
return op->body;
}
return stmt;
}
private:
IRVisitorWithAnalyzer* bound_analyzer_;
};
// The specific tensor data layout is not determined before
// StorageFlatten pass. We use buffer_bind_scope
// to specify before hand we want to bind a subregion
// of tensor to a symbolic buffer, which get used in extern.
//
// Example:
//
// realize A in range [i*4, extent=10) {
// bind Ab to A in [i*4+1, extent=4) {
// call_func(Ab.ptr, Ab.shape[0])
// }
// }
//
// After StorageFlatten
//
// alloc A[10]
// call(A + 1, 4)
//
// Buffer is a protocol to declare specific
// data layout and shape we expect.
// So this function need to check:
// - If the bind range is within the realize range
// - If we can match the requirement of buffer
// - Remap variables such as Ab.ptr to the actual value.
//
// Here are a few possible failure cases:
// - Buffer is declared to have constant shape,
// but we try to bind it to a different one.
// - Buffer is declared to be compact(no strides)
// but this binded region is a subregion of
// a matrix(tensor), which means it requires strides.
//
// We do support a few relaxed case, such as binding a
// region with shape [1, 1, n, m] to buffer with shape [n, m]
PrimFunc StorageFlatten(PrimFunc func, int cache_line_size, bool create_bound_attributes) {
// Only apply this pass to TIR from TE schedules. Because this is a
// per-function attribute, we can't just check it once for the
// entire module and apply the Sequential transform.
Optional<Bool> from_legacy_te_schedule = func->GetAttr("from_legacy_te_schedule", Bool(false));
if (from_legacy_te_schedule.value()) {
auto seq = transform::Sequential(
{
BufferShapeLegalize::Pass(),
BufferStrideLegalize::Pass(),
ThreadScopePropagate::Pass(),
BufferBindUnwrapper::Pass(),
ApplyLayoutTransforms::Pass(),
StorageFlattener::Pass(cache_line_size, create_bound_attributes),
AssertSimplifier::Pass(),
},
"tir.StorageFlatten_impl");
GlobalVar dummy_func_name("dummy_func");
IRModule mod(Map<GlobalVar, BaseFunc>({{dummy_func_name, func}}));
mod = seq(mod);
return Downcast<PrimFunc>(mod->Lookup(dummy_func_name));
} else {
return func;
}
}
namespace transform {
TVM_REGISTER_GLOBAL("tir.transform.ApplyLayoutTransforms")
.set_body_typed(ApplyLayoutTransforms::Pass);
// TODO(tvm-team): consolidate configs to the PassContext
Pass StorageFlatten(int cache_line_size, bool create_bound_attributes) {
auto pass_func = [=](PrimFunc f, IRModule m, PassContext ctx) {
return StorageFlatten(std::move(f), cache_line_size, create_bound_attributes);
};
return CreatePrimFuncPass(pass_func, 0, "tir.StorageFlatten", {});
}
TVM_REGISTER_GLOBAL("tir.transform.StorageFlatten").set_body_typed(StorageFlatten);
} // namespace transform
} // namespace tir
} // namespace tvm