blob: 950b5acc34fbf08bddbceaf81cd53117f1ef940b [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#include <tvm/runtime/device_api.h> // For `kAllocAlignment`
#include <algorithm>
#include <utility>
#include "./utils.h"
namespace tvm {
namespace script {
namespace printer {
ffi::Map<ffi::String, ExprDoc> BufferAttrs(
tirx::BufferVar buffer, const AccessPath& buffer_p, const Frame& frame, const IRDocsifier& d,
BufferVarDefinition var_definitions, ffi::Optional<Expr> data = std::nullopt,
bool stringify_undefined_shape = false, std::unordered_set<tirx::Var> stringify_shape_vars = {},
std::unordered_set<tirx::Var> stringify_compound_shape_vars = {}) {
using tvm::tirx::Var;
using tvm::tirx::VarNode;
ffi::Map<ffi::String, ExprDoc> kwargs;
ffi::Array<ExprDoc> var_def_lhs;
ffi::Array<ExprDoc> var_def_rhs;
// Step 0. Set up statistics
std::unordered_map<const ffi::Object*, int> use_count;
auto update_use_count = [&](const Expr& e) {
tirx::PostOrderVisit(e, [&](const ffi::ObjectRef& n) {
if (const VarNode* var = n.as<VarNode>()) {
++use_count[var];
}
});
};
update_use_count(buffer->elem_offset);
if (data.has_value()) {
update_use_count(data.value());
}
for (const PrimExpr& e : buffer->strides) {
update_use_count(e);
}
for (const PrimExpr& e : buffer->shape) {
update_use_count(e);
}
auto is_new_var = [&](const Expr& e) { return e->IsInstance<VarNode>() && !d->IsVarDefined(e); };
auto add_out_of_line_var_def = [&](const Var& var, const AccessPath& var_p) {
TVM_FFI_ICHECK(!d->IsVarDefined(var));
ExprDoc lhs = DefineVar(var, frame, d);
lhs->source_paths.push_back(var_p);
var_def_lhs.push_back(lhs);
var_def_rhs.push_back(PrintVarCreation(var, var_p, d));
};
auto try_inline_def = [&](const Expr& e, const AccessPath& e_p,
std::function<ExprDoc()> inline_f) {
TVM_FFI_ICHECK(is_new_var(e));
Var var = e.as_or_throw<Var>();
if (use_count[var.get()] == 1) {
d->Define(e, frame, inline_f);
return true;
} else {
add_out_of_line_var_def(var, e_p);
return false;
}
};
// Step 1. Handle `buffer.shape`
{
const ffi::Array<PrimExpr>& shape = buffer->shape;
AccessPath shape_p = buffer_p->Attr("shape");
int n = shape.size();
ffi::Array<ExprDoc> results;
results.reserve(n);
for (int i = 0; i < n; ++i) {
PrimExpr e = shape[i];
AccessPath e_p = shape_p->ArrayItem(i);
bool contains_new_var = false;
bool contains_compound_shape_var = false;
std::unordered_set<Var> vars_in_shape;
tirx::PostOrderVisit(e, [&](const ffi::ObjectRef& obj) {
if (const auto* var_node = obj.as<VarNode>()) {
Var var = ffi::GetRef<Var>(var_node);
vars_in_shape.insert(var);
contains_new_var =
contains_new_var || !d->IsVarDefined(var) || stringify_shape_vars.count(var);
contains_compound_shape_var =
contains_compound_shape_var || stringify_compound_shape_vars.count(var);
}
});
if (is_new_var(e)) {
add_out_of_line_var_def(e.as_or_throw<Var>(), e_p);
}
ExprDoc result = d->AsDoc<ExprDoc>(e, e_p);
bool is_bare_compound_shape_var =
e.as<VarNode>() && stringify_compound_shape_vars.count(e.as_or_throw<Var>());
bool stringify_compound_expr = contains_compound_shape_var && !is_bare_compound_shape_var;
results.push_back((stringify_undefined_shape && contains_new_var) || stringify_compound_expr
? ExprStringDoc(result, e_p)
: result);
// A quoted shape expression defines every Var it contains. Do not quote
// later dimensions merely because they reuse a Var introduced here.
for (const Var& var : vars_in_shape) {
stringify_shape_vars.erase(var);
}
}
kwargs.Set("shape", TupleDoc(results));
}
// Step 2. Handle `buffer.dtype`
{
DLDataType default_buf_dtype = d->cfg->buffer_dtype;
if (buffer->dtype->dtype != default_buf_dtype) {
kwargs.Set("dtype", LiteralDoc::DataType(buffer->dtype->dtype, buffer_p->Attr("dtype")));
}
}
// Step 3. Handle `buffer.data`
// For tmem scope, DeclBuffer does not accept `data` (it auto-creates the data var).
bool is_tmem_scope = buffer.scope() == "tmem";
bool is_inline_data = false;
if (!is_tmem_scope && data.has_value()) {
Expr source = data.value();
if (is_new_var(source)) {
if (var_definitions >= BufferVarDefinition::DataPointer) {
is_inline_data = try_inline_def(source, buffer_p->Attr("data"), [=]() {
return d->AsDoc<ExprDoc>(buffer, buffer_p)->Attr("data");
});
} else {
add_out_of_line_var_def(source.as_or_throw<Var>(), buffer_p->Attr("data"));
}
}
if (!is_inline_data) {
kwargs.Set("data", d->AsDoc<ExprDoc>(source, buffer_p->Attr("data")));
}
}
// Step 4. Handle `buffer.strides`
if (!buffer->strides.empty()) {
const ffi::Array<PrimExpr>& strides = buffer->strides;
AccessPath strides_p = buffer_p->Attr("strides");
int n = strides.size();
ffi::Array<ExprDoc> results;
results.reserve(n);
for (int i = 0; i < n; ++i) {
PrimExpr e = strides[i];
AccessPath e_p = strides_p->ArrayItem(i);
if (is_new_var(e)) {
if (try_inline_def(e, e_p, [=]() {
return d->AsDoc<ExprDoc>(buffer, buffer_p)
->Attr("strides")[{LiteralDoc::Int(i, std::nullopt)}];
})) {
results.push_back(LiteralDoc::Str(e.as_or_throw<Var>()->name, e_p));
continue;
}
}
results.push_back(d->AsDoc<ExprDoc>(e, e_p));
}
kwargs.Set("strides", TupleDoc(results));
}
// Step 5. Handle `buffer.elem_offset`
bool needs_print_factor = false;
if (const auto* int_imm = buffer->elem_offset.as<IntImmNode>()) {
if (int_imm->value != 0 ||
int_imm->ty.as_or_throw<PrimType>()->dtype != buffer->DefaultIndexType()) {
kwargs.Set("elem_offset",
d->AsDoc<ExprDoc>(buffer->elem_offset, //
buffer_p->Attr("elem_offset")));
}
} else if (is_new_var(buffer->elem_offset)) {
try_inline_def(buffer->elem_offset, buffer_p->Attr("elem_offset"),
[=]() { return d->AsDoc<ExprDoc>(buffer, buffer_p)->Attr("elem_offset"); });
needs_print_factor = true;
} else {
kwargs.Set("elem_offset",
d->AsDoc<ExprDoc>(buffer->elem_offset, //
buffer_p->Attr("elem_offset")));
}
// Step 6. Handle `buffer.scope`
{
ffi::String scope = buffer.scope();
if (scope != "global") {
kwargs.Set("scope",
LiteralDoc::Str(scope, buffer_p->Attr("data")->Attr("ty")->Attr("storage_scope")));
}
}
// Step 7. Handle `buffer.data_alignment`
if (buffer->data_alignment != runtime::kAllocAlignment) {
kwargs.Set("align", LiteralDoc::Int(buffer->data_alignment, buffer_p->Attr("data_alignment")));
}
// Step 8. Handle `buffer.offset_factor`
if (needs_print_factor || buffer->offset_factor != 1) {
kwargs.Set("offset_factor",
LiteralDoc::Int(buffer->offset_factor, buffer_p->Attr("offset_factor")));
}
// Step 9. Handle `buffer.layout`. Track the enclosing PrimFunc's `s_tir`
// attr — in `s_tir=True` mode the parser fills `layout=None` by default,
// in `s_tir=False` (tirx) mode it fills `DefaultLayout(shape)`. Mirror
// that here so the implicit default is omitted and the non-default value
// is emitted explicitly (round-trips safely under `StructuralEqual`).
bool enclosing_s_tir = false;
for (const auto& f : d->frames) {
if (const auto* tir_f = f.as<TIRFrameNode>()) {
if (auto func = tir_f->tirx.as<tirx::PrimFuncNode>()) {
if (func->attrs->dict.count(tvm::attr::kSTir)) {
enclosing_s_tir = true;
}
break;
}
}
}
if (buffer->layout.has_value()) {
bool is_default =
ffi::StructuralEqual()(buffer->layout, tirx::TileLayoutNode::DefaultLayout(buffer->shape));
if (!is_default) {
kwargs.Set("layout", d->AsDoc<ExprDoc>(buffer->layout, buffer_p->Attr("layout")));
}
} else if (!enclosing_s_tir) {
kwargs.Set("layout", LiteralDoc::None(buffer_p->Attr("layout")));
}
// Step 10. Handle `buffer.allocated_addr`
if (!buffer->allocated_addr.empty()) {
if (buffer->allocated_addr.size() == 1) {
// Unwrap single-element array: DeclBuffer expects Optional<PrimExpr>, not Array.
// For BufferLoad from scalar buffers, we must explicitly print buf[idx] because
// the scalar shorthand (which drops the index) produces just the variable name,
// and the parser resolves that to a BufferVar object rather than a PrimExpr value.
PrimExpr addr = buffer->allocated_addr[0];
AccessPath addr_p = buffer_p->Attr("allocated_addr")->ArrayItem(0);
if (const auto* bl = addr.as<tirx::BufferLoadNode>()) {
// Ensure the buffer variable is defined (may emit a T.Buffer(...) statement).
d->AsDoc<ExprDoc>(bl->buffer, addr_p->Attr("buffer"));
// Get the variable name bound to this buffer.
ffi::Optional<ExprDoc> buf_var = d->GetVarDoc(bl->buffer);
TVM_FFI_ICHECK(buf_var.has_value())
<< "BufferVar in allocated_addr is not defined: " << bl->buffer;
// Build var[indices] explicitly instead of going through the default BufferLoad
// printer, which would use the scalar shorthand and drop the index.
int n_idx = bl->indices.size();
ffi::Array<Doc> idx_docs;
idx_docs.reserve(n_idx);
for (int i = 0; i < n_idx; ++i) {
idx_docs.push_back(
d->AsDoc<ExprDoc>(bl->indices[i], addr_p->Attr("indices")->ArrayItem(i)));
}
kwargs.Set("allocated_addr", buf_var.value()[idx_docs]);
} else {
kwargs.Set("allocated_addr", d->AsDoc<ExprDoc>(addr, addr_p));
}
} else {
kwargs.Set("allocated_addr",
d->AsDoc<ExprDoc>(buffer->allocated_addr, buffer_p->Attr("allocated_addr")));
}
}
if (var_def_lhs.size() == 1) {
frame->stmts.push_back(AssignDoc(var_def_lhs[0], var_def_rhs[0], std::nullopt));
} else if (var_def_lhs.size() > 1) {
frame->stmts.push_back(AssignDoc(TupleDoc(var_def_lhs), TupleDoc(var_def_rhs), std::nullopt));
}
return kwargs;
}
ExprDoc BufferCall(const ExprDoc& prefix, const ffi::Map<ffi::String, ExprDoc>& attrs,
ffi::Array<ExprDoc> args) {
ffi::Array<ffi::String> kwargs_keys;
ffi::Array<ExprDoc> kwargs_values;
for (ffi::String s : {"shape", "dtype"}) {
if (ffi::Optional<ExprDoc> doc = attrs.Get(s)) {
args.push_back(doc.value());
}
}
for (ffi::String s : {"data", "strides", "elem_offset", "scope", "align", "offset_factor",
"layout", "allocated_addr"}) {
if (ffi::Optional<ExprDoc> doc = attrs.Get(s)) {
kwargs_keys.push_back(s);
kwargs_values.push_back(doc.value());
}
}
return prefix->Call(args, kwargs_keys, kwargs_values);
}
ExprDoc BufferDecl(const tirx::BufferVar& buffer, const ffi::String& method,
const ffi::Array<ExprDoc>& args, const AccessPath& p, const Frame& frame,
const IRDocsifier& d, BufferVarDefinition var_definitions,
ffi::Optional<Expr> data) {
auto prefix = TIR(d, method);
auto attrs = BufferAttrs(buffer, p, frame, d, var_definitions, data);
if (method == "alloc_buffer") {
if (buffer.IsScalar()) {
// The buffer can be allocated by the alloc_scalar function
auto dtype = d->AsDoc<ExprDoc>(buffer->dtype, p->Attr("dtype"));
if (buffer.scope() == "shared") {
// shared_scalar
prefix = TIR(d, "shared_scalar");
attrs = ffi::Map<ffi::String, ExprDoc>({{"dtype", dtype}});
} else if (buffer.scope() == "local") {
// local_scalar
prefix = TIR(d, "local_scalar");
attrs = ffi::Map<ffi::String, ExprDoc>({{"dtype", dtype}});
} else {
// alloc_scalar
prefix = TIR(d, "alloc_scalar");
auto scope = d->AsDoc<ExprDoc>(buffer.scope(), p->Attr("scope"));
attrs = ffi::Map<ffi::String, ExprDoc>({{"dtype", dtype}, {"scope", scope}});
}
} else {
if (buffer.scope() == "shared") {
// alloc_shared
prefix = TIR(d, "alloc_shared");
attrs.erase("scope");
} else if (buffer.scope() == "local") {
// alloc_local
prefix = TIR(d, "alloc_local");
attrs.erase("scope");
}
}
} else if (method == "decl_buffer") {
if (buffer.IsScalar(false)) {
// decl_scalar
prefix = TIR(d, "decl_scalar");
auto dtype = d->AsDoc<ExprDoc>(buffer->dtype, p->Attr("dtype"));
auto scope = d->AsDoc<ExprDoc>(buffer.scope(), p->Attr("scope"));
auto elem_offset = d->AsDoc<ExprDoc>(buffer->elem_offset, p->Attr("elem_offset"));
attrs = ffi::Map<ffi::String, ExprDoc>(
{{"dtype", dtype}, {"scope", scope}, {"elem_offset", elem_offset}});
if (data.has_value()) {
attrs.Set("data", d->AsDoc<ExprDoc>(data.value(), p->Attr("data")));
}
}
}
return BufferCall(prefix, attrs, args);
}
ExprDoc BufferAttn(const tirx::BufferVar& buffer, const AccessPath& p, const Frame& frame,
const IRDocsifier& d, std::unordered_set<tirx::Var> stringify_shape_vars,
std::unordered_set<tirx::Var> stringify_compound_shape_vars) {
ffi::Map<ffi::String, ExprDoc> attrs =
BufferAttrs(buffer, p, frame, d, BufferVarDefinition::MatchBuffer, std::nullopt, true,
std::move(stringify_shape_vars), std::move(stringify_compound_shape_vars));
if (!attrs.count("dtype")) {
attrs.Set("dtype", LiteralDoc::DataType(buffer->dtype->dtype, p->Attr("dtype")));
}
return BufferCall(TIR(d, "Buffer"), attrs, {});
}
ffi::Array<Doc> BufferIndices(const ffi::Array<PrimExpr>& indices, const AccessPath& p,
const IRDocsifier& d) {
int n = indices.size();
ffi::Array<Doc> indices_doc;
indices_doc.reserve(n);
for (int i = 0; i < n; ++i) {
if (const auto* ramp = indices[i].as<tirx::RampNode>()) {
if (const auto* stride = ramp->stride.as<IntImmNode>()) {
AccessPath ramp_p = p->Attr("indices")->ArrayItem(i);
AccessPath stride_p = ramp_p->Attr("stride");
ExprDoc start = d->AsDoc<ExprDoc>(ramp->base, //
ramp_p->Attr("base"));
ExprDoc stop = d->AsDoc<ExprDoc>(ramp->base + ramp->lanes * ramp->stride, //
ramp_p->Attr("lanes"));
ffi::Optional<ExprDoc> step = std::nullopt;
if (stride->value != 1) {
step = d->AsDoc<ExprDoc>(ramp->stride, ramp_p->Attr("stride"));
}
indices_doc.push_back(SliceDoc(start, stop, step));
continue;
}
}
indices_doc.push_back(d->AsDoc<ExprDoc>(indices[i], p->Attr("indices")->ArrayItem(i)));
}
return indices_doc;
}
ffi::Array<Doc> BufferSlices(const ffi::Array<Range>& region, const AccessPath& p,
const IRDocsifier& d) {
int n = region.size();
ffi::Array<Doc> indices;
indices.reserve(n);
for (int i = 0; i < n; ++i) {
Range range = region[i];
AccessPath range_p = p->ArrayItem(i);
ExprDoc min = d->AsDoc<ExprDoc>(range->min, range_p->Attr("min"));
if (tirx::is_one(range->extent)) {
indices.push_back(min);
} else {
ExprDoc max = d->AsDoc<ExprDoc>(range->min + range->extent, range_p->Attr("extent"));
indices.push_back(SliceDoc(min, max, std::nullopt));
}
}
return indices;
}
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::BufferRegion>(
"", [](tirx::BufferRegion buffer_region, AccessPath p, IRDocsifier d) -> Doc {
ExprDoc prefix = d->AsDoc<ExprDoc>(buffer_region->buffer, p->Attr("buffer"));
return prefix[BufferSlices(buffer_region->region, p->Attr("region"), d)];
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::BufferStore>( //
"", [](tirx::BufferStore store, AccessPath p, IRDocsifier d) -> Doc {
ExprDoc buffer = d->AsDoc<ExprDoc>(store->buffer, p->Attr("buffer"));
ExprDoc value = d->AsDoc<ExprDoc>(store->value, p->Attr("value"));
// special case for scalar buffers
if ((store->buffer.IsScalar(true) || store->buffer.IsScalar(false)) &&
!store->predicate.has_value()) {
// TVM_FFI_ICHECK(store->indices.size() == 1 && tirx::is_zero(store->indices[0]))
// << "1-dim buffer with shape (1,) store with indices other than [0] is not "
// "supported";
ffi::Optional<ExprDoc> doc = d->GetVarDoc(store->buffer);
TVM_FFI_ICHECK(doc.has_value())
<< "buffer is not defined in the environment: " << store->buffer;
return AssignDoc(doc.value(), value, std::nullopt);
}
// Use .vstore(...) syntax when there is a predicate
if (store->predicate.has_value()) {
ExprDoc indices = d->AsDoc<ExprDoc>(store->indices, p->Attr("indices"));
ExprDoc predicate = d->AsDoc<ExprDoc>(store->predicate, p->Attr("predicate"));
return ExprStmtDoc(
buffer->Attr("vstore")->Call({indices, value}, {"predicate"}, {predicate}));
}
return AssignDoc(
/*lhs=*/buffer[BufferIndices(store->indices, p->Attr("indices"), d)],
/*rhs=*/value, std::nullopt);
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::BufferLoad>( //
"", [](tirx::BufferLoad load, AccessPath p, IRDocsifier d) -> Doc {
ExprDoc buffer = d->AsDoc<ExprDoc>(load->buffer, p->Attr("buffer"));
// special case for scalar
if ((load->buffer.IsScalar(true) || load->buffer.IsScalar(false)) &&
!load->predicate.has_value()) {
// TVM_FFI_ICHECK(load->indices.size() == 1 && tirx::is_zero(load->indices[0]))
// << "Scalar buffer load with indices other than [0] is not supported";
ffi::Optional<ExprDoc> doc = d->GetVarDoc(load->buffer);
TVM_FFI_ICHECK(doc.has_value())
<< "Scalar buffer is not defined in the environment: " << load->buffer;
return doc.value();
}
// Use .vload(...) syntax when there is a predicate
if (load->predicate.has_value()) {
ExprDoc indices = d->AsDoc<ExprDoc>(load->indices, p->Attr("indices"));
ExprDoc predicate = d->AsDoc<ExprDoc>(load->predicate, p->Attr("predicate"));
return buffer->Attr("vload")->Call({indices}, {"predicate"}, {predicate});
}
return buffer[BufferIndices(load->indices, p->Attr("indices"), d)];
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::Axis>("", [](tirx::Axis axis, AccessPath p, IRDocsifier d) -> Doc {
return LiteralDoc::Str(axis->name, p->Attr("name"));
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::Iter>("", [](tirx::Iter iter, AccessPath p, IRDocsifier d) -> Doc {
return TIR(d, "Iter")->Call({d->AsDoc<ExprDoc>(iter->extent, p->Attr("extent")),
d->AsDoc<ExprDoc>(iter->stride, p->Attr("stride")),
d->AsDoc<ExprDoc>(iter->axis->name, p->Attr("axis"))},
{}, {});
});
Doc PrintTileLayout(tirx::TileLayout layout, IRDocsifier d, AccessPath p) {
using OpKind = OperationDocNode::Kind;
// `value @ Axis.<name>`, but elide `@m` (the default memory axis).
auto bind_axis = [&](ExprDoc value, const tirx::Axis& axis) -> ExprDoc {
if (axis->name == "m") return value;
return OperationDoc(OpKind::kMatMul, {value, IdDoc("Axis")->Attr(axis->name)});
};
// Build `head[(e0, e1, ...) : (s0@a0, s1@a1, ...)]` (or 1D shorthand
// `head[e : s@a]`) from a list of Iters.
auto iters_to_index = [&](ExprDoc head, const ffi::Array<tirx::Iter>& iters) -> ExprDoc {
ffi::Array<ExprDoc> extents;
ffi::Array<ExprDoc> strides;
for (const auto& iter : iters) {
extents.push_back(d->AsDoc<ExprDoc>(iter->extent, p->Attr("extent")));
ExprDoc s = d->AsDoc<ExprDoc>(iter->stride, p->Attr("stride"));
strides.push_back(bind_axis(s, iter->axis));
}
ExprDoc start = (extents.size() == 1) ? extents[0] : ExprDoc(TupleDoc(extents));
ExprDoc stop = (strides.size() == 1) ? strides[0] : ExprDoc(TupleDoc(strides));
return IndexDoc(head, {SliceDoc(start, stop, std::nullopt)});
};
// Degenerate case: no shard / replica iters. Fall back to from_iters so the
// offset (if any) still round-trips.
if (layout->shard.size() == 0 && layout->replica.size() == 0) {
ffi::Array<ffi::String> keys;
ffi::Array<ExprDoc> values;
if (layout->offset.size() > 0) {
ffi::Array<ExprDoc> offset_keys, offset_values;
for (const auto& [axis, off] : layout->offset) {
offset_keys.push_back(LiteralDoc::Str(axis->name, p->Attr("axis")));
offset_values.push_back(d->AsDoc<ExprDoc>(off, p->Attr("offset")));
}
keys.push_back("offset");
values.push_back(DictDoc(offset_keys, offset_values));
}
return TIRx(d, "TileLayout")->Attr("from_iters")->Call({}, keys, values);
}
// Compose `Tx.S[..] [+ Tx.R[..]] [+ offset_expr]`.
auto add_term = [&](ffi::Optional<ExprDoc>& acc, ExprDoc term) {
if (acc) {
acc = ExprDoc(OperationDoc(OpKind::kAdd, {acc.value(), term}));
} else {
acc = term;
}
};
ffi::Optional<ExprDoc> spec;
if (layout->shard.size() > 0) {
add_term(spec, iters_to_index(TIRx(d, "S"), layout->shard));
}
if (layout->replica.size() > 0) {
add_term(spec, iters_to_index(TIRx(d, "R"), layout->replica));
}
if (layout->offset.size() > 0) {
// Sort by axis name so the printed text is deterministic across builds
// (`ffi::Map` iteration order is implementation-defined).
std::vector<std::pair<tirx::Axis, PrimExpr>> sorted_offset(layout->offset.begin(),
layout->offset.end());
std::sort(sorted_offset.begin(), sorted_offset.end(),
[](const auto& a, const auto& b) { return a.first->name < b.first->name; });
// Build the offset as a single arithmetic expression first, then add it
// to the spec in one `+`. Chaining `spec + term1 + term2` would re-enter
// `_LayoutSpec.__add__` with the second term and overwrite the offset
// (see `python/tvm/tirx/layout.py::_LayoutSpec.__add__`), silently
// dropping all but the last axis term. Combining the terms first lets
// `_OnAxis.__add__` / `_OffsetExpr.__add__` accumulate them correctly.
ffi::Optional<ExprDoc> off_doc;
for (const auto& [axis, off] : sorted_offset) {
ExprDoc term = bind_axis(d->AsDoc<ExprDoc>(off, p->Attr("offset")), axis);
if (off_doc) {
off_doc = ExprDoc(OperationDoc(OpKind::kAdd, {off_doc.value(), term}));
} else {
off_doc = term;
}
}
add_term(spec, off_doc.value());
}
return TIRx(d, "TileLayout")->Call({spec.value()}, {}, {});
}
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable) //
.set_dispatch<tirx::TileLayout>("",
[](tirx::TileLayout layout, AccessPath p, IRDocsifier d)
-> Doc { return PrintTileLayout(layout, d, p); });
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable) //
.set_dispatch<tirx::ComposeLayout>(
"", [](tirx::ComposeLayout layout, AccessPath p, IRDocsifier d) -> Doc {
auto per_element = LiteralDoc::Int(layout->per_element, p->Attr("per_element"));
auto swizzle_len = LiteralDoc::Int(layout->swizzle_len, p->Attr("swizzle_len"));
auto atom_len = LiteralDoc::Int(layout->atom_len, p->Attr("atom_len"));
auto tile_doc = d->AsDoc<ExprDoc>(layout->tile_layout, p->Attr("tile_layout"));
ffi::Array<ffi::String> kwargs_keys;
ffi::Array<ExprDoc> kwargs_values;
if (!layout->swizzle_inner) {
kwargs_keys.push_back("swizzle_inner");
kwargs_values.push_back(
LiteralDoc::Boolean(layout->swizzle_inner, p->Attr("swizzle_inner")));
}
return TIRx(d, "ComposeLayout")
->Call({per_element, swizzle_len, atom_len, tile_doc}, kwargs_keys, kwargs_values);
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::MatchBufferRegion>(
"", [](tirx::MatchBufferRegion stmt, AccessPath p, IRDocsifier d) -> Doc {
Frame frame = d->frames.back();
ExprDoc lhs = DefineBuffer(stmt->buffer, frame, d);
ExprDoc src_buffer = d->AsDoc<ExprDoc>(stmt->source, p->Attr("source"));
ExprDoc rhs = BufferDecl(stmt->buffer, "match_buffer", {src_buffer}, p->Attr("buffer"),
d->frames.back(), d, BufferVarDefinition::MatchBuffer);
return AssignDoc(lhs, rhs, std::nullopt);
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::ProducerLoad>( //
"", [](tirx::ProducerLoad load, AccessPath p, IRDocsifier d) -> Doc {
ExprDoc prefix = IdDoc(load->producer->GetNameHint());
return prefix[BufferIndices(load->indices, p->Attr("indices"), d)];
});
TVM_SCRIPT_REPR(tirx::BufferRegionNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::BufferLoadNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::BufferStoreNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::BufferTypeNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::IterNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::TileLayoutNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::ComposeLayoutNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::MatchBufferRegionNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::ProducerLoadNode, ReprPrintTIR);
} // namespace printer
} // namespace script
} // namespace tvm