blob: 4f25f2bd773412f497c09cfbc00f01f43af9f740 [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 <algorithm>
#include "../../../tirx/transform/ir_utils.h" // For `GetPtrStorageScope`
#include "./utils.h"
namespace tvm {
namespace script {
namespace printer {
Doc DoConciseScoping(const ffi::Optional<ExprDoc>& lhs, const ExprDoc& rhs,
ffi::Array<StmtDoc>* stmts, bool concise_scoping) {
if (concise_scoping) {
if (lhs.has_value()) {
stmts->insert(stmts->begin(), AssignDoc(lhs.value(), rhs, std::nullopt));
} else {
stmts->insert(stmts->begin(), ExprStmtDoc(rhs));
}
return StmtBlockDoc(*stmts);
} else {
return ScopeDoc(lhs, rhs, *stmts);
}
}
bool AllowConciseScoping(const IRDocsifier& d, const ffi::ObjectRef& obj) {
if (d->cfg.defined()) {
if (d->cfg->obj_to_annotate.count(obj)) {
// if the object requires annotation, do not fold this frame
return false;
}
}
TVM_FFI_ICHECK(!d->frames.empty());
if (const auto* f = d->frames.back().as<TIRFrameNode>()) {
return f->allow_concise_scoping;
}
TVM_FFI_THROW(NotImplementedError) << "fragment printing";
TVM_FFI_UNREACHABLE();
}
bool IsAncestorOfAllVarUse(const tirx::Stmt& node, const ffi::ObjectRef& var,
const IRDocsifier& d) {
if (!d->common_prefix.count(var.get())) {
return false;
}
const std::vector<const ffi::Object*>& path = d->common_prefix.at(var.get());
for (auto it = path.rbegin(); it != path.rend(); ++it) {
if (*it == node.get()) {
return true;
}
}
return false;
}
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::TilePrimitiveCall>(
"", [](tirx::TilePrimitiveCall op_call, AccessPath p, IRDocsifier d) -> Doc {
static const OpAttrMap<tirx::TScriptPrinterName>& op_names =
Op::GetAttrMap<tirx::TScriptPrinterName>("TScriptPrinterName");
auto op = op_call->op;
if (op_names.count(op) == 0) {
LOG(WARNING) << "No TScriptPrinterName attribute for " << op->name;
}
static const auto& category_map = Op::GetAttrMap<tirx::TIRxOpCategory>("TIRxOpCategory");
bool is_tile_primitive = category_map.get(op, ffi::String("")) == "tile_primitive";
TVM_FFI_ICHECK(is_tile_primitive)
<< "Only tile primitive ops can be used in tirx::TilePrimitiveCall";
ffi::String name = op_names.get(op, op->name);
// Per-call execution scope is printed as a namespace prefix on the op,
// e.g. ``T.warp.copy(...)``. ``warpgroup`` prints as ``wg``. The
// default ``thread`` scope prints through the explicit tile namespace,
// e.g. ``T.tile.copy(...)``, so canonical script only needs the full
// TIRx dialect import. ``Tx`` remains a handwritten shorthand for
// ``T.tile`` and ``T.<scope>`` tile calls.
auto scope_ns = [](tirx::ScopeKind k) -> ffi::Optional<ffi::String> {
switch (k) {
case tirx::ScopeKind::kWarp:
return ffi::String("warp");
case tirx::ScopeKind::kWarpgroup:
return ffi::String("wg");
case tirx::ScopeKind::kCta:
return ffi::String("cta");
case tirx::ScopeKind::kCluster:
return ffi::String("cluster");
default: // kThread -> no prefix
return std::nullopt;
}
};
auto scoped_callee = [&](const ffi::String& op_name) -> ExprDoc {
ffi::Optional<ffi::String> ns = scope_ns(op_call->scope->kind);
if (ns.has_value()) {
return TIRx(d, ns.value())->Attr(op_name);
}
return TIRx(d, "tile")->Attr(op_name);
};
if (!op.same_as(tirx::compose_op())) {
// Trim trailing None args (e.g. optional bias=None, scale=None)
size_t n_args = op_call->args.size();
while (n_args > 0 &&
op_call->args[n_args - 1].type_index() == ffi::TypeIndex::kTVMFFINone) {
--n_args;
}
// Detect in-place unary ops: after trimming Nones, if exactly 2 args
// and args[0]/args[1] refer to the same buffer region, collapse to 1 arg
bool inplace_unary = false;
if (n_args == 2) {
auto dst_opt = op_call->args[0].as<tirx::BufferRegion>();
auto src_opt = op_call->args[1].as<tirx::BufferRegion>();
if (dst_opt.has_value() && src_opt.has_value() &&
dst_opt.value()->buffer.same_as(src_opt.value()->buffer) &&
StructuralEqual()(dst_opt.value()->region, src_opt.value()->region)) {
inplace_unary = true;
}
}
ffi::Array<Doc> args;
for (size_t i = 0; i < n_args; ++i) {
if (inplace_unary && i == 1) continue; // skip duplicate src
args.push_back(d->AsDoc<Doc>(op_call->args[i], p->Attr("args")->ArrayItem(i)));
}
ffi::Optional<ExprDoc> disp = std::nullopt;
if (op_call->dispatch.has_value()) {
disp = LiteralDoc::Str(op_call->dispatch.value(), p->Attr("dispatch"));
}
return OpCallDoc(scoped_callee(name), args,
d->AsDoc<DictDoc>(op_call->workspace, p->Attr("workspace")),
d->AsDoc<DictDoc>(op_call->config, p->Attr("config")), disp);
} else {
With<TIRFrame> f(d, op_call);
ffi::Array<tirx::Stmt> stmts;
for (size_t i = 0, n = op_call->args.size(); i < n; ++i) {
stmts.push_back(op_call->args[i].as_or_throw<tirx::Stmt>());
}
tirx::SeqStmt seq_stmt(stmts);
AsDocBody(seq_stmt, p->Attr("args"), f->get(), d);
// Build kwargs: workspace, dispatch, then flatten config
ffi::Array<ffi::String> kw_keys;
ffi::Array<ExprDoc> kw_values;
if (!op_call->workspace.empty()) {
kw_keys.push_back("workspace");
kw_values.push_back(d->AsDoc<DictDoc>(op_call->workspace, p->Attr("workspace")));
}
if (op_call->dispatch.has_value()) {
kw_keys.push_back("dispatch");
kw_values.push_back(LiteralDoc::Str(op_call->dispatch.value(), p->Attr("dispatch")));
}
using POO = std::pair<ffi::String, ffi::Any>;
std::vector<POO> items{op_call->config.begin(), op_call->config.end()};
std::sort(items.begin(), items.end(),
[](const POO& a, const POO& b) { return a.first < b.first; });
for (const auto& kv : items) {
kw_keys.push_back(kv.first);
kw_values.push_back(
d->AsDoc<ExprDoc>(kv.second, p->Attr("config")->MapItem(kv.first)));
}
return ScopeDoc(std::nullopt, scoped_callee("compose_op")->Call({}, kw_keys, kw_values),
(*f)->stmts);
}
});
TVM_SCRIPT_REPR(tirx::TilePrimitiveCallNode, ReprPrintTIR);
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::Evaluate>("", [](tirx::Evaluate eval, AccessPath p, IRDocsifier d) -> Doc {
ExprDoc value = d->AsDoc<ExprDoc>(eval->value, p->Attr("value"));
const auto* call = eval->value.as<CallNode>();
if (call && !call->op.same_as(tirx::builtin::buffer_data())) {
return ExprStmtDoc(value);
}
return ExprStmtDoc(TIR(d, "evaluate")->Call({value}));
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::Return>("", [](tirx::Return stmt, AccessPath p, IRDocsifier d) -> Doc {
ExprDoc value = d->AsDoc<ExprDoc>(stmt->value, p->Attr("value"));
return ReturnDoc(value);
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::Bind>("", [](tirx::Bind stmt, AccessPath p, IRDocsifier d) -> Doc {
// Step 1. Type annotation
TVM_FFI_ICHECK(!stmt->var->ty.IsMissing())
<< "Type annotation is required for variable: " << stmt->var->name;
ffi::Optional<ExprDoc> type_doc = d->AsDoc<ExprDoc>(stmt->var->ty, //
p->Attr("var")->Attr("ty"));
if (const auto* tuple_type = stmt->var->ty.as<TupleTypeNode>()) {
if (tuple_type->fields.empty()) {
type_doc = std::nullopt;
}
}
// Step 2. RHS
ExprDoc rhs = d->AsDoc<ExprDoc>(stmt->value, p->Attr("value"));
// Step 3. LHS - Bind is flat, define var if new, otherwise just assign
if (!d->IsVarDefined(stmt->var)) {
TVM_FFI_ICHECK(!d->frames.empty());
ExprDoc lhs = DefineVar(stmt->var, d->frames.back(), d);
ExprDoc let_ann = type_doc.has_value()
? ExprDoc(IndexDoc(TIR(d, "let"), {type_doc.value()}))
: TIR(d, "let");
return AssignDoc(lhs, rhs, let_ann);
} else {
ExprDoc lhs = d->AsDoc<ExprDoc>(stmt->var, p->Attr("var"));
return AssignDoc(lhs, rhs, std::nullopt);
}
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::AssertStmt>(
"", [](tirx::AssertStmt stmt, AccessPath p, IRDocsifier d) -> Doc {
ExprDoc cond = d->AsDoc<ExprDoc>(stmt->condition, p->Attr("condition"));
// Always emit the canonical tuple form: assert cond, ("Kind", ["part0", "part1", ...])
ffi::Array<ExprDoc> parts;
auto parts_path = p->Attr("message_parts");
for (size_t i = 0; i < stmt->message_parts.size(); ++i) {
parts.push_back(d->AsDoc<ExprDoc>(stmt->message_parts[i], parts_path->ArrayItem(i)));
}
ExprDoc kind_doc = d->AsDoc<ExprDoc>(stmt->error_kind, p->Attr("error_kind"));
return AssertDoc(cond, TupleDoc({kind_doc, ListDoc(parts)}));
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::While>("", [](tirx::While stmt, AccessPath p, IRDocsifier d) -> Doc {
ExprDoc cond = d->AsDoc<ExprDoc>(stmt->condition, p->Attr("condition"));
With<TIRFrame> f(d, stmt);
AsDocBody(stmt->body, p->Attr("body"), f->get(), d);
return WhileDoc(cond, (*f)->stmts);
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::Break>("", [](tirx::Break stmt, AccessPath p, IRDocsifier d) -> Doc {
return BreakDoc();
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::Continue>("", [](tirx::Continue stmt, AccessPath p, IRDocsifier d) -> Doc {
return ContinueDoc();
});
namespace {
/*!
* \brief Find the parent buffer named by a child's explicit data projection.
* \param child The child buffer.
* \param data The child's explicit source pointer, if any.
* \param d The IRDocsifier.
* \return A list of candidate parent buffers.
*/
std::vector<tirx::BufferVar> FindParentBuffers(const tirx::BufferVar& child,
const ffi::Optional<Expr>& data,
const IRDocsifier& d) {
if (!data.has_value()) {
return {};
}
const auto* call = data.value().as<CallNode>();
if (call == nullptr || !call->op.same_as(tirx::builtin::buffer_data()) ||
call->args.size() != 1) {
return {};
}
auto parent_var = call->args[0].as<tirx::Var>();
if (!parent_var.has_value() || !parent_var.value()->ty.as<tirx::BufferTypeNode>()) {
return {};
}
tirx::BufferVar parent(parent_var.value());
if (parent.same_as(child) || !d->GetVarDoc(parent).has_value()) {
return {};
}
return {parent};
}
/*!
* \brief Check if a layout is the default layout for a given shape.
*/
bool IsDefaultLayout(const ffi::Optional<tirx::Layout>& layout, const ffi::Array<PrimExpr>& shape) {
if (!layout.has_value()) return false;
return StructuralEqual()(layout.value(), tirx::TileLayoutNode::DefaultLayout(shape));
}
/*!
* \brief Try to produce a DeclBuffer sugar expression for the given child buffer
* with respect to a specific parent buffer.
*
* Returns std::nullopt if no sugar pattern matches.
*/
ffi::Optional<ExprDoc> TryDeclBufferSugarWithParent(const tirx::BufferVar& child,
const AccessPath& p, const IRDocsifier& d,
const tirx::BufferVar& parent,
bool require_same_layout) {
ffi::Optional<ExprDoc> parent_doc = d->GetVarDoc(parent);
if (!parent_doc.has_value()) return std::nullopt;
ExprDoc pdoc = parent_doc.value();
tirx::ExprDeepEqual expr_equal;
// Check elem_offset equality
bool same_elem_offset = expr_equal(child->elem_offset, parent->elem_offset);
// Check dtype equality
bool same_dtype = (child->dtype == parent->dtype);
// Check shape equality
bool same_shape = (child->shape.size() == parent->shape.size());
if (same_shape) {
for (size_t i = 0; i < child->shape.size(); ++i) {
if (!expr_equal(child->shape[i], parent->shape[i])) {
same_shape = false;
break;
}
}
}
bool same_strides = (child->strides.size() == parent->strides.size());
if (same_strides) {
for (size_t i = 0; i < child->strides.size(); ++i) {
if (!expr_equal(child->strides[i], parent->strides[i])) {
same_strides = false;
break;
}
}
}
bool child_is_default = IsDefaultLayout(child->layout, child->shape);
bool parent_is_default = IsDefaultLayout(parent->layout, parent->shape);
// NOTE: an earlier sugar printed rank-preserving aliases with a different
// elem_offset as ``parent[slices]``. That print is not roundtrippable: it
// reparses as a BufferRegion, not a Buffer, so any later Buffer use of the
// alias (stores, views) breaks. Such aliases now print as plain
// T.decl_buffer, which reparses exactly.
// Differences in these Buffer fields cannot be expressed by the alias sugar
// below, so conservatively fall back to T.decl_buffer.
// Shape, strides, elem_offset, dtype, and layout are checked by each helper
// because those are the fields that individual transformations may change.
//
// The explicit data projection was checked by FindParentBuffers. name/span
// do not participate in structural equality, and BufferTypeNode has no
// axis-separators field (unlike tir::Buffer).
bool same_common_metadata = child.scope() == parent.scope() &&
child->data_alignment == parent->data_alignment &&
child->offset_factor == parent->offset_factor &&
StructuralEqual()(child->allocated_addr, parent->allocated_addr);
if (!same_common_metadata) return std::nullopt;
// --- (b) Local: parent has thread axes and child spans its physical storage ---
if (same_elem_offset && same_dtype && same_strides && !parent_is_default &&
parent->layout.has_value() && child->layout.has_value()) {
if (auto* parent_tile = parent->layout.value().as<tirx::TileLayoutNode>()) {
if (parent_tile->HasThreadAxis()) {
// Compute the raw physical storage span after filtering thread axes.
std::vector<tirx::Iter> storage_shard;
std::vector<tirx::Iter> storage_replica;
ffi::Map<tirx::Axis, PrimExpr> storage_offset;
for (const auto& iter : parent_tile->shard) {
if (!iter->axis->IsThreadAxis()) {
storage_shard.push_back(iter);
}
}
for (const auto& iter : parent_tile->replica) {
if (!iter->axis->IsThreadAxis()) {
storage_replica.push_back(iter);
}
}
for (const auto& [axis, off] : parent_tile->offset) {
if (!axis->IsThreadAxis()) {
storage_offset.Set(axis, off);
}
}
tirx::TileLayout expected_storage(
ffi::Array<tirx::Iter>(storage_shard.begin(), storage_shard.end()),
ffi::Array<tirx::Iter>(storage_replica.begin(), storage_replica.end()), storage_offset);
PrimExpr storage_span = expected_storage->GetSpan(ffi::Optional<ffi::String>());
PrimExpr storage_size = expected_storage->GetSize(ffi::Optional<ffi::String>());
PrimExpr child_total = IntImm::Int32(1);
for (const PrimExpr& dim : child->shape) {
child_total = child_total * dim;
}
arith::Analyzer analyzer;
bool default_physical =
child_is_default && analyzer->CanProveEqual(child_total, storage_span);
bool child_has_thread_axis = false;
if (const auto* child_tile = child->layout.value().as<tirx::TileLayoutNode>()) {
child_has_thread_axis = child_tile->HasThreadAxis();
}
bool explicit_override = !default_physical && !child_has_thread_axis;
if (default_physical || explicit_override) {
PrimExpr expected_extent = default_physical ? storage_span : storage_size;
bool auto_shape =
child->shape.size() == 1 && analyzer->CanProveEqual(child->shape[0], expected_extent);
ffi::Array<ExprDoc> args;
if (!auto_shape) {
for (size_t i = 0; i < child->shape.size(); ++i) {
args.push_back(d->AsDoc<ExprDoc>(child->shape[i],
p->Attr("buffer")->Attr("shape")->ArrayItem(i)));
}
}
ffi::Array<ffi::String> kwargs_keys;
ffi::Array<ExprDoc> kwargs_values;
if (explicit_override) {
kwargs_keys.push_back("layout");
kwargs_values.push_back(
d->AsDoc<ExprDoc>(child->layout.value(), p->Attr("buffer")->Attr("layout")));
}
return pdoc->Attr("local")->Call(args, kwargs_keys, kwargs_values);
}
}
}
}
// --- (c) View(dtype): different dtype, same elem_offset ---
if (same_elem_offset && !same_dtype && child->shape.size() == parent->shape.size()) {
// Verify shape compatibility with dtype reinterpret cast
int child_bits = child->dtype.bits();
int parent_bits = parent->dtype.bits();
bool shapes_compatible = true;
// All dims except last must match
for (size_t i = 0; i + 1 < child->shape.size(); ++i) {
if (!expr_equal(child->shape[i], parent->shape[i])) {
shapes_compatible = false;
break;
}
}
if (shapes_compatible && !child->shape.empty()) {
auto* child_last = child->shape.back().as<IntImmNode>();
auto* parent_last = parent->shape.back().as<IntImmNode>();
if (child_last && parent_last) {
if (child_bits > parent_bits) {
// Cast up: child_last = parent_last / ratio
int ratio = child_bits / parent_bits;
shapes_compatible = (parent_last->value == child_last->value * ratio);
} else {
// Cast down: child_last = parent_last * ratio
int ratio = parent_bits / child_bits;
shapes_compatible = (child_last->value == parent_last->value * ratio);
}
} else {
shapes_compatible = false;
}
}
// Also verify the parent's layout is compatible with the pack/unpack operation
if (shapes_compatible && parent->layout.has_value()) {
if (auto* ptile = parent->layout.value().as<tirx::TileLayoutNode>()) {
if (!ptile->shard.empty() && child_bits > parent_bits) {
// Cast up requires pack: last shard iter must have stride=1
// and extent divisible by ratio
const auto& last_iter = ptile->shard.back();
auto* last_stride = last_iter->stride.as<IntImmNode>();
auto* last_extent = last_iter->extent.as<IntImmNode>();
int ratio = child_bits / parent_bits;
if (!last_stride || last_stride->value != 1 || !last_extent ||
last_extent->value % ratio != 0) {
shapes_compatible = false;
}
}
}
}
if (shapes_compatible) {
ExprDoc dtype_doc =
LiteralDoc::Str(DType2Str(child->dtype->dtype), p->Attr("buffer")->Attr("dtype"));
return pdoc->Attr("view")->Call({dtype_doc});
}
}
// --- (d) Permute: child shape is a permutation of parent shape, same elem_offset ---
if (same_elem_offset && same_dtype && !same_shape &&
child->shape.size() == parent->shape.size()) {
// Try to find a permutation
std::vector<int> perm(child->shape.size(), -1);
std::vector<bool> used(parent->shape.size(), false);
bool is_permutation = true;
for (size_t i = 0; i < child->shape.size(); ++i) {
bool found = false;
for (size_t j = 0; j < parent->shape.size(); ++j) {
if (!used[j] && expr_equal(child->shape[i], parent->shape[j])) {
perm[i] = j;
used[j] = true;
found = true;
break;
}
}
if (!found) {
is_permutation = false;
break;
}
}
// Check it's not identity
bool is_identity = is_permutation;
if (is_permutation) {
for (size_t i = 0; i < perm.size(); ++i) {
if (perm[i] != static_cast<int>(i)) {
is_identity = false;
break;
}
}
}
if (is_permutation && !is_identity) {
// Verify the layout matches permutation by comparing shard iters directly
bool layout_matches = false;
if (parent->layout.has_value() && child->layout.has_value()) {
auto* parent_tile = parent->layout.value().as<tirx::TileLayoutNode>();
auto* child_tile = child->layout.value().as<tirx::TileLayoutNode>();
if (parent_tile && child_tile && parent_tile->shard.size() == child_tile->shard.size()) {
StructuralEqual seq;
layout_matches = true;
for (size_t i = 0; i < perm.size(); ++i) {
if (!seq(child_tile->shard[i], parent_tile->shard[perm[i]])) {
layout_matches = false;
break;
}
}
// Also check replica and offset are unchanged
if (layout_matches) {
layout_matches = seq(child_tile->replica, parent_tile->replica) &&
seq(child_tile->offset, parent_tile->offset);
}
}
}
if (layout_matches) {
ffi::Array<ExprDoc> args;
for (int idx : perm) {
args.push_back(LiteralDoc::Int(idx, p->Attr("buffer")->Attr("shape")));
}
return pdoc->Attr("permute")->Call(args);
}
}
}
// --- (e) Partition: child has 2*parent_ndim dims with grid+tile strides ---
if (same_elem_offset && same_dtype && !parent->shape.empty() &&
child->shape.size() == 2 * parent->shape.size() && !child->strides.empty() &&
child->strides.size() == 2 * parent->shape.size()) {
size_t ndim = parent->shape.size();
// Compute parent's row-major strides
std::vector<int64_t> parent_rm_strides(ndim);
int64_t stride = 1;
bool all_const = true;
for (int i = static_cast<int>(ndim) - 1; i >= 0; --i) {
parent_rm_strides[i] = stride;
if (auto* s = parent->shape[i].as<IntImmNode>()) {
stride *= s->value;
} else {
all_const = false;
break;
}
}
if (all_const) {
bool is_partition = true;
for (size_t i = 0; i < ndim; ++i) {
auto* grid_dim = child->shape[i].as<IntImmNode>();
auto* tile_dim = child->shape[ndim + i].as<IntImmNode>();
auto* parent_dim = parent->shape[i].as<IntImmNode>();
auto* grid_stride = child->strides[i].as<IntImmNode>();
auto* tile_stride = child->strides[ndim + i].as<IntImmNode>();
if (!grid_dim || !tile_dim || !parent_dim || !grid_stride || !tile_stride) {
is_partition = false;
break;
}
// grid × tile == parent dim
if (grid_dim->value * tile_dim->value != parent_dim->value) {
is_partition = false;
break;
}
// inner strides match parent's row-major strides
if (tile_stride->value != parent_rm_strides[i]) {
is_partition = false;
break;
}
// grid stride == tile_dim × inner stride
if (grid_stride->value != tile_dim->value * tile_stride->value) {
is_partition = false;
break;
}
}
if (is_partition) {
ffi::Array<ExprDoc> tuple_elems;
for (size_t i = 0; i < ndim; ++i) {
tuple_elems.push_back(
d->AsDoc<ExprDoc>(child->shape[i], p->Attr("buffer")->Attr("shape")->ArrayItem(i)));
}
return pdoc->Attr("partition")->Call({}, {"num_tiles"}, {TupleDoc(tuple_elems)});
}
}
}
// --- (f) View(*shape, layout=L): different shape/layout, same dtype and elem_offset ---
if (same_elem_offset && same_dtype && !same_shape) {
// Buffer.view(...) copies the parent's strides onto the child (see
// python/tvm/tirx/buffer.py:view). If parent has strides but child
// doesn't (or vice versa), the sugar can't faithfully round-trip
// through view — fall back to T.decl_buffer where strides is an
// explicit kwarg.
if (!same_strides) return std::nullopt;
ffi::Array<ExprDoc> args;
ffi::Array<ffi::String> kwargs_keys;
ffi::Array<ExprDoc> kwargs_values;
for (size_t i = 0; i < child->shape.size(); ++i) {
args.push_back(
d->AsDoc<ExprDoc>(child->shape[i], p->Attr("buffer")->Attr("shape")->ArrayItem(i)));
}
// Check if layout differs
bool same_layout = false;
if (child->layout.has_value() && parent->layout.has_value()) {
same_layout = StructuralEqual()(child->layout.value(), parent->layout.value());
} else if (!child->layout.has_value() && !parent->layout.has_value()) {
same_layout = true;
}
// First pass prefers a parent whose layout matches structurally, so the
// sugar prints as a bare reshape instead of restating the layout.
if (require_same_layout && !same_layout) return std::nullopt;
// Default layouts are shape-specific objects, but a default-to-default
// reshape is still represented by view(*shape) without an explicit layout.
if (!same_layout && !(child_is_default && parent_is_default)) {
// Buffer.view(..., layout=None) means "inherit the parent layout", so it
// cannot reconstruct a layout-less child from a laid-out parent.
if (!child->layout.has_value()) return std::nullopt;
kwargs_keys.push_back("layout");
kwargs_values.push_back(
d->AsDoc<ExprDoc>(child->layout.value(), p->Attr("buffer")->Attr("layout")));
}
return pdoc->Attr("view")->Call(args, kwargs_keys, kwargs_values);
}
return std::nullopt;
}
/*!
* \brief Try to produce a DeclBuffer sugar expression, trying all parent buffer candidates.
*/
ffi::Optional<ExprDoc> TryDeclBufferSugar(const tirx::BufferVar& child, const AccessPath& p,
const ffi::Optional<Expr>& data, const IRDocsifier& d) {
auto parents = FindParentBuffers(child, data, d);
for (const auto& parent : parents) {
if (auto sugar = TryDeclBufferSugarWithParent(child, p, d, parent,
/*require_same_layout=*/true)) {
return sugar;
}
}
for (const auto& parent : parents) {
if (auto sugar = TryDeclBufferSugarWithParent(child, p, d, parent,
/*require_same_layout=*/false)) {
return sugar;
}
}
return std::nullopt;
}
Doc DeclBufferDoc(tirx::DeclBuffer stmt, AccessPath p, IRDocsifier d,
BufferVarDefinition var_definitions) {
// Try sugar detection when syntax_sugar is enabled
if (d->cfg->syntax_sugar) {
if (auto sugar = TryDeclBufferSugar(stmt->buffer, p, stmt->data, d)) {
ExprDoc lhs = DefineBuffer(stmt->buffer, d->frames.back(), d);
return AssignDoc(lhs, sugar.value(), std::nullopt);
}
}
ExprDoc rhs = BufferDecl(stmt->buffer, "decl_buffer", {}, p->Attr("buffer"), d->frames.back(), d,
var_definitions, stmt->data);
ExprDoc lhs = DefineBuffer(stmt->buffer, d->frames.back(), d);
return AssignDoc(lhs, rhs, std::nullopt);
}
} // namespace
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::DeclBuffer>( //
"", [](tirx::DeclBuffer stmt, AccessPath p, IRDocsifier d) -> Doc {
return DeclBufferDoc(stmt, p, d, BufferVarDefinition::None);
});
namespace {
Doc AllocBufferDoc(tirx::AllocBuffer stmt, AccessPath p, IRDocsifier d) {
if (d->cfg->syntax_sugar && stmt->buffer.IsScalar(true)) {
ExprDoc lhs = DefineBuffer(stmt->buffer, d->frames.back(), d);
ExprDoc type_ann = TIR(d, DType2Str(stmt->buffer->dtype->dtype));
return AssignDoc(lhs, std::nullopt, type_ann);
}
ExprDoc rhs = BufferDecl(stmt->buffer, "alloc_buffer", {}, p->Attr("buffer"), d->frames.back(), d,
BufferVarDefinition::DataPointer);
// alloc_buffer carries an `annotations` field on the IR node that BufferDecl
// doesn't know about. When non-empty, append it as an `annotations=...`
// kwarg on the emitted call so round-trip preserves the annotation map.
if (!stmt->annotations.empty()) {
if (const auto* call = rhs.as<CallDocNode>()) {
ffi::Array<ffi::String> new_keys = call->kwargs_keys;
ffi::Array<ExprDoc> new_values = call->kwargs_values;
new_keys.push_back("annotations");
new_values.push_back(d->AsDoc<ExprDoc>(stmt->annotations, p->Attr("annotations")));
rhs = CallDoc(call->callee, call->args, new_keys, new_values);
}
}
ExprDoc lhs = DefineBuffer(stmt->buffer, d->frames.back(), d);
return AssignDoc(lhs, rhs, std::nullopt);
}
} // namespace
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::AllocBuffer>( //
"", [](tirx::AllocBuffer stmt, AccessPath p, IRDocsifier d) -> Doc {
return AllocBufferDoc(stmt, p, d);
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::IfThenElse>( //
"", [](tirx::IfThenElse stmt, AccessPath p, IRDocsifier d) -> Doc {
ExprDoc cond = d->AsDoc<ExprDoc>(stmt->condition, p->Attr("condition"));
ffi::Array<StmtDoc> then_branch;
ffi::Array<StmtDoc> else_branch;
if (stmt->then_case.defined()) {
With<TIRFrame> f(d, stmt->then_case);
AsDocBody(stmt->then_case, p->Attr("then_case"), f->get(), d);
then_branch = (*f)->stmts;
}
if (stmt->else_case.has_value()) {
With<TIRFrame> f(d, stmt->else_case.value());
AsDocBody(stmt->else_case.value(), p->Attr("else_case"), f->get(), d);
else_branch = (*f)->stmts;
}
return IfDoc(cond, then_branch, else_branch);
});
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::SeqStmt>("", [](tirx::SeqStmt stmt, AccessPath p, IRDocsifier d) -> Doc {
With<TIRFrame> f(d, stmt);
AsDocBody(stmt, p, f->get(), d);
return StmtBlockDoc((*f)->stmts);
});
void InsertEnvThread(const tirx::IterVar& iter_var, const AccessPath& iter_var_p,
const IRDocsifier& d) {
Frame f = FindLowestVarDef(iter_var->var, d).value();
DefineVar(iter_var->var, f, d);
ExprDoc rhs = TIR(d, "env_thread")
->Call({LiteralDoc::Str(iter_var->thread_tag, //
iter_var_p->Attr("thread_tag"))});
ExprDoc lhs = d->AsDoc<ExprDoc>(iter_var->var, iter_var_p->Attr("var"));
f->stmts.push_back(AssignDoc(lhs, rhs, std::nullopt));
}
ExprDoc DocsifyLaunchThread(const tirx::AttrStmt& attr_stmt, const AccessPath& attr_stmt_p,
ffi::Optional<tirx::Var>* define_var, const IRDocsifier& d) {
tirx::IterVar iter_var = attr_stmt->node.as_or_throw<tirx::IterVar>();
AccessPath iter_var_p = attr_stmt_p->Attr("node");
ExprDoc var_doc{ffi::UnsafeInit()};
if (d->IsVarDefined(iter_var->var)) {
var_doc = d->AsDoc<ExprDoc>(iter_var->var, iter_var_p->Attr("var"));
} else if (IsAncestorOfAllVarUse(attr_stmt, iter_var->var, d)) {
var_doc = LiteralDoc::Str(iter_var->thread_tag, iter_var_p->Attr("thread_tag"));
*define_var = iter_var->var;
} else {
InsertEnvThread(iter_var, iter_var_p, d);
var_doc = d->AsDoc<ExprDoc>(iter_var->var, iter_var_p->Attr("var"));
}
return TIR(d, "launch_thread")
->Call({
var_doc,
d->AsDoc<ExprDoc>(attr_stmt->value, attr_stmt_p->Attr("value")),
});
}
/*! \brief Check whether an AttrStmt has node=0 (the dict-attr pattern). */
static bool IsDictAttrPattern(const tirx::AttrStmt& stmt) {
if (auto int_value = stmt->node.as<int64_t>()) {
return int_value.value() == 0;
}
return false;
}
TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
.set_dispatch<tirx::AttrStmt>( //
"", [](tirx::AttrStmt stmt, AccessPath stmt_p, IRDocsifier d) -> Doc {
bool concise = AllowConciseScoping(d, stmt);
ffi::Optional<ExprDoc> lhs = std::nullopt;
ffi::Optional<ExprDoc> rhs = std::nullopt;
ffi::Optional<tirx::Var> define_var = std::nullopt;
tirx::Stmt body = stmt->body;
AccessPath body_p = stmt_p->Attr("body");
if (stmt->attr_key == "thread_extent" || stmt->attr_key == "virtual_thread") {
if (stmt->node.as<tirx::IterVarNode>()) {
rhs = DocsifyLaunchThread(stmt, stmt_p, &define_var, d);
}
}
if (stmt->attr_key == "tirx_hint") {
if (auto map_node = stmt->node.as<ffi::Map<ffi::String, ffi::Any>>()) {
ffi::Array<ExprDoc> args;
ffi::Array<ffi::String> kwargs_keys;
ffi::Array<ExprDoc> kwargs_values;
for (const auto& [k, v] : map_node.value()) {
if (k == "message") {
auto s = v.as<ffi::String>().value();
args.push_back(LiteralDoc::Str(s, stmt_p->Attr("node")));
} else {
kwargs_keys.push_back(k);
kwargs_values.push_back(d->AsDoc<ExprDoc>(v, stmt_p->Attr("node")));
}
}
rhs = TIR(d, "hint")->Call(args, kwargs_keys, kwargs_values);
}
}
if (!rhs.has_value()) {
// Try to collapse consecutive dict-attr-pattern AttrStmts into T.attr({...})
if (IsDictAttrPattern(stmt)) {
ffi::Array<ExprDoc> keys;
ffi::Array<ExprDoc> values;
tirx::AttrStmt cur = stmt;
AccessPath cur_p = stmt_p;
while (true) {
keys.push_back(LiteralDoc::Str(cur->attr_key, cur_p->Attr("attr_key")));
values.push_back(d->AsDoc<ExprDoc>(cur->value, cur_p->Attr("value")));
if (auto next = cur->body.as<tirx::AttrStmt>()) {
if (IsDictAttrPattern(next.value())) {
cur = next.value();
cur_p = cur_p->Attr("body");
continue;
}
}
body = cur->body;
body_p = cur_p->Attr("body");
break;
}
rhs = TIR(d, "attr")->Call({DictDoc(keys, values)});
} else {
rhs = TIR(d, "attr")->Call({
d->AsDoc<ExprDoc>(stmt->node, stmt_p->Attr("node")),
LiteralDoc::Str(stmt->attr_key, stmt_p->Attr("attr_key")),
d->AsDoc<ExprDoc>(stmt->value, stmt_p->Attr("value")),
});
}
}
With<TIRFrame> f(d, stmt);
if (define_var.has_value()) {
lhs = DefineVar(define_var.value(), *f, d);
}
AsDocBody(body, body_p, f->get(), d);
return DoConciseScoping(lhs, rhs.value(), &(*f)->stmts, concise);
});
TVM_SCRIPT_REPR(tirx::BindNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::AttrStmtNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::AssertStmtNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::WhileNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::AllocBufferNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::ReturnNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::BreakNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::ContinueNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::DeclBufferNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::SeqStmtNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::IfThenElseNode, ReprPrintTIR);
TVM_SCRIPT_REPR(tirx::EvaluateNode, ReprPrintTIR);
} // namespace printer
} // namespace script
} // namespace tvm