blob: f33f70e8ac0655da746dc9d188b104d7e528c523 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/*!
* \file src/relax/transform/convert_layout.cc
* \brief Automatic layout conversion pass, especially for axis swapping.
*/
#include <tvm/ffi/cast.h>
#include <tvm/ffi/extra/serialization.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/relax/expr_functor.h>
#include <tvm/relax/nested_msg.h>
#include <tvm/relax/op_attr_types.h>
#include <tvm/relax/transform.h>
#include <tvm/tirx/index_map.h>
#include "../op/tensor/manipulate.h"
#include "infer_layout_utils.h"
#include "utils.h"
namespace tvm {
namespace relax {
using tirx::IndexMap;
using tirx::SLayout;
using LayoutCb = tvm::relax::transform::LayoutCb;
/*!
* \brief Main logic to convert the layout of conv2d. Other ops
* can adapt to such layout conversion following conv2d accordingly.
*
* Structurally speaking, a Relax function is composed of a series of VarBinding and
* MatchCast. And a specific class of VarBindings is the basic unit we want to rewrite.
* Formally, they are of the form:
*
* var = Call(Op, [args], attrs)
*
* where Op is a specific op we want to rewrite, and attrs is the attributes of the op.
* var and args are all exprs with type Tensor or Tuple of Tensors. They might
* be vars, constants, or Tuple of vars and constants.
*
* We register the layout inference function for each op (FRelaxInferLayout), which accepts the
* current call, the desired layout of conv2d ops, and the layout map of previous vars. The result
* of the layout inference function is contained in an InferLayoutOutput object, which contains 3
* fields: input_layouts, output_layouts, and attr, which represents the expected input layout,
* output_layout and converted attrs of the new op call.
*
* The rewrite pass does the rewriting in a single forward pass, where for each Call(Op),
* we collect the current SLayout of each input var, and let the InferLayout function to infer the
* desired layout of the output. The rewriter will use these info to convert
* the layout of inputs and attrs of the op call, and note down the new layout of the output.
*
* The desired layout of conv2d ops is a map from the name of the op to the desired layout of the
* desired feature map, weight and output. For example, if we want to convert the layout of conv2d
* from NCHW to NHWC, we can set the desired layout of conv2d to be {"conv2d": ["NHWC", "OHWI"]}.
*
* The way we represent the layout of a var is a NLayout object, which is a nested tuple of SLayout.
* The incoming layout of the module will be set as the default layout (We use ABCD... as the
* default) Note that for operators like conv, pool, people typically use NHWC to refer to the axes.
* But to be generic and support more operators, we use ABCD... to refer to the axes.
*
* Note that currently the layout conversion of conv2d only support axis swapping, such as NCHW to
* NWHC. Packed layout such as NCHW to NCHW4c is not supported now.
*/
class LayoutConvertMutator : public ExprMutator {
public:
explicit LayoutConvertMutator(
const ffi::Map<ffi::String, ffi::Array<ffi::String>>& desired_layouts, LayoutCb layout_cb)
: desired_layouts_(desired_layouts), layout_cb_(layout_cb) {}
private:
ffi::Array<int64_t> LayoutToIntegers(const SLayout& layout) {
ffi::Array<int64_t> ret;
LayoutDecision src = InitialLayoutDecision(layout.ndim());
for (size_t i = 0; i < layout.ndim(); ++i) {
ret.push_back(static_cast<int64_t>(src->layout.IndexOf(layout[i])));
}
return ret;
}
IndexMap LayoutIndexMap(int ndim, const SLayout& src_layout, const SLayout& desired_layout) {
tirx::SBijectiveLayout todesired(src_layout, desired_layout);
ffi::Optional<IndexMap> inverse_index_map;
ffi::Array<tvm::tirx::Var> initial_indices;
ffi::Array<PrimExpr> initial_indices_expr;
initial_indices.reserve(ndim);
for (int i = 0; i < ndim; ++i) {
auto var = tvm::tirx::Var("i" + std::to_string(i), PrimType::Int(32));
initial_indices.push_back(var);
initial_indices_expr.push_back(var.as_or_throw<PrimExpr>());
}
ffi::Array<PrimExpr> desired_shape = todesired.ForwardIndex(initial_indices_expr);
return IndexMap(initial_indices.Map(
[](tvm::tirx::Var var) { return var.as_or_throw<tvm::tirx::PrimVar>(); }),
desired_shape, std::move(inverse_index_map));
}
Expr RewriteExpr(const Expr& expr, const NLayout& to) {
auto fvisitleaf = [&](const Expr& expr, std::array<NLayout, 2> layouts) -> Expr {
NLayout from = layouts[0], to = layouts[1];
if (NLayoutEqual()(from, to) || layouts[0].LeafValue()->layout.name() == "") return expr;
// If not both from and to are unknown, then none of them can be unknown.
TVM_FFI_ICHECK(!NLayoutEqual()(from, LayoutDecision::InitUnknownDim()) &&
!NLayoutEqual()(to, LayoutDecision::InitUnknownDim()))
<< "Cannot convert when exactly one of the layouts is unknown";
const auto* tensor = GetTypeAs<TensorTypeNode>(expr);
TVM_FFI_ICHECK(tensor != nullptr) << "Expect a tensor, but got: " << expr;
if (from.LeafValue()->layout.ndim() == to.LeafValue()->layout.ndim()) {
SLayout axes = TransposeLike(InitialLayoutDecision(tensor->ndim)->layout,
from.LeafValue()->layout, to.LeafValue()->layout);
return permute_dims(expr, LayoutToIntegers(axes));
} else {
auto index_map = LayoutIndexMap(from.LeafValue()->layout.ndim(), from.LeafValue()->layout,
to.LeafValue()->layout);
ffi::ObjectPtr<LayoutTransformAttrs> attrs = ffi::make_object<LayoutTransformAttrs>();
attrs->index_map = ffi::FromJSONGraph(ffi::ToJSONGraph(index_map)).as_or_throw<IndexMap>();
const Op& layout_transform_op_ = Op::Get("relax.layout_transform");
auto ret_expr =
Call(Type::Missing(), layout_transform_op_, {expr}, Attrs{std::move(attrs)}, {});
return ret_expr;
}
};
return TransformTupleLeaf<LayoutDecision>(
VarReplacer::Replace(expr, var_remap_),
std::array<NLayout, 2>({GetNLayout(var_layout_map_, expr), to}), fvisitleaf);
}
ffi::Array<Expr> RewriteArgs(const ffi::Array<Expr>& args, const ffi::Array<NLayout>& to) {
// The `ffi::Array<Expr> args` array contains both tensor and
// non-tensor arguments, where the `ffi::Array<NLayout> to` array only
// contains tensor arguments. The number of tensor arguments in
// `args` should match the full extent of `to`.
TVM_FFI_ICHECK_LE(to.size(), args.size());
std::vector<Expr> new_args;
for (size_t i = 0; i < args.size(); ++i) {
Expr arg = args[i];
if (i < to.size()) {
arg = RewriteExpr(arg, to[i]);
}
new_args.push_back(arg);
}
return new_args;
}
void VisitBinding(const Binding& binding) final {
// Emit the binding
ExprMutator::VisitBinding(binding);
// The layout is default to be initial if not rewritten.
if (var_layout_map_.find(binding->var) == var_layout_map_.end()) {
var_layout_map_[binding->var] = InitialNLayout(binding->var);
}
}
Expr VisitVars_(const Var& var) {
// We encounter a var use outside of inferrable regions, we rewrite it to initial layout.
return RewriteExpr(var, InitialNLayout(var));
}
Expr VisitExpr_(const VarNode* op) final { return VisitVars_(ffi::GetRef<Var>(op)); }
bool HasUnknownDimTensor(const NLayout& nlayout) {
bool find = false;
auto fvisit = [&](const LayoutDecision& layout) {
find = find | (NLayoutEqual()(layout, LayoutDecision::InitUnknownDim()));
};
ForEachLeaf<LayoutDecision>(nlayout, fvisit);
return find;
}
bool HasUnknownDimTensor(const ffi::Array<Expr>& args) {
for (const auto& arg : args) {
if (IsNestedTensor(arg)) {
if (HasUnknownDimTensor(GetNLayout(var_layout_map_, arg))) {
return true;
}
}
}
return false;
}
ffi::Optional<InferLayoutOutput> GetInferLayoutInfo(
const CallNode* call_node,
const ffi::Map<ffi::String, ffi::Array<ffi::String>>& desired_layouts,
const LayoutCb& layout_cb, const VarLayoutMap& var_layout_map) {
const OpNode* op_node = call_node->op.as<OpNode>();
if (op_node == nullptr) return std::nullopt;
Op op = ffi::GetRef<Op>(op_node).as_or_throw<Op>();
const auto attr_map = Op::GetAttrMap<FRelaxInferLayout>("FRelaxInferLayout");
if (attr_map.count(op) && !HasUnknownDimTensor(call_node->args)) {
// If the op has FRelaxInferLayout, and all the input tensors have known ndim
FRelaxInferLayout f = attr_map[op];
auto call = ffi::GetRef<Call>(call_node);
if (layout_cb != nullptr) {
auto custom_layouts = layout_cb(call);
return f(call, custom_layouts, var_layout_map);
} else {
return f(call, desired_layouts, var_layout_map);
}
} else {
// Otherwise, we use the default policy.
return std::nullopt;
}
}
void VisitBinding_(const VarBindingNode* binding, const CallNode* call_node) final {
ffi::Optional<InferLayoutOutput> res =
GetInferLayoutInfo(call_node, desired_layouts_, layout_cb_, var_layout_map_);
ffi::ObjectPtr<CallNode> new_call = ffi::make_object<CallNode>(*call_node);
new_call->ty = Type::Missing();
if (!res.has_value() ||
(!IsNestedTensor(binding->var) && !binding->var->IsInstance<DataflowVarNode>())) {
// Default policy: use the initial layout.
// When we don't have the infer layout info, or it's a non-tensor global var binding.
std::vector<NLayout> input_layout;
for (const auto& arg : call_node->args) {
input_layout.push_back(InitialNLayout(arg));
}
ffi::Array<Expr> new_args = RewriteArgs(call_node->args, std::move(input_layout));
new_call->args = std::move(new_args);
ReEmitBinding(binding, builder_->Normalize(Call(new_call)));
// update the layout map
var_layout_map_[binding->var] = InitialNLayout(binding->var);
} else {
// Convert the layout according to the inferred layout output.
ffi::Array<Expr> new_args = RewriteArgs(call_node->args, res.value()->input_layouts);
for (const auto& [i, arg] : res.value()->new_args) {
new_args.Set(i->value, arg);
}
new_call->args = std::move(new_args);
new_call->attrs = std::move(res.value()->new_attrs);
Expr cur_call = builder_->Normalize(Call(new_call));
if (binding->var->IsInstance<DataflowVarNode>()) {
// Dataflow var, we emit the rewritten call.
ReEmitBinding(binding, cur_call);
// update the layout map
var_layout_map_[binding->var] = res.value()->output_layouts[0];
} else {
// Global var (tensor), we rewrite it to initial layout
TVM_FFI_ICHECK(IsNestedTensor(binding->var));
if (!NLayoutEqual()(res.value()->output_layouts[0], InitialNLayout(binding->var))) {
Var new_var = builder_->Emit(cur_call);
var_layout_map_[new_var] = res.value()->output_layouts[0];
cur_call = builder_->Normalize(RewriteExpr(new_var, InitialNLayout(binding->var)));
}
ReEmitBinding(binding, cur_call);
// update the layout map
var_layout_map_[binding->var] = InitialNLayout(binding->var);
}
}
}
void VisitBinding_(const VarBindingNode* binding, const TupleNode* val) final {
std::vector<NLayout> input_layout;
for (const auto& field : val->fields) {
if (binding->var->IsInstance<DataflowVarNode>()) {
// Df var: Use the current realized layout to group the tuple;
input_layout.push_back(GetNLayout(var_layout_map_, field));
} else {
// Global var: Use the initial layout to group the tuple;
input_layout.push_back(InitialNLayout(field));
}
}
ffi::Array<Expr> new_fields = RewriteArgs(val->fields, std::move(input_layout));
if (IsNestedTensor(binding->var)) {
ReEmitBinding(binding, builder_->Normalize(Tuple(new_fields)));
var_layout_map_[binding->var] = input_layout;
}
}
void VisitBinding_(const VarBindingNode* binding, const TupleGetItemNode* val) final {
NLayout input_layout = binding->var->IsInstance<DataflowVarNode>()
? GetNLayout(var_layout_map_, val->tuple)
: InitialNLayout(val->tuple);
ReEmitBinding(binding, builder_->Normalize(
TupleGetItem(RewriteExpr(val->tuple, input_layout), val->index)));
// update the layout map
var_layout_map_[binding->var] = input_layout.NestedArray()[val->index];
}
void VisitBinding_(const MatchCastNode* binding) final {
if (!binding->var->IsInstance<DataflowVarNode>()) {
ExprMutator::VisitBinding_(binding);
return;
}
NLayout from_layout = InitialNLayout(binding->value);
NLayout input_layout = GetNLayout(var_layout_map_, binding->value);
auto fvisitleaf = [&](const Type& ty, std::array<NLayout, 2> layouts) -> Type {
NLayout from = layouts[0], to = layouts[1];
if (NLayoutEqual()(from, to)) return ty;
// If not both from and to are unknown, then none of them can be unknown.
TVM_FFI_ICHECK(!NLayoutEqual()(from, LayoutDecision::InitUnknownDim()) &&
!NLayoutEqual()(to, LayoutDecision::InitUnknownDim()))
<< "Cannot convert when exactly one of the layouts is unknown";
const TensorTypeNode* tensor_ty = ty.as<TensorTypeNode>();
TVM_FFI_ICHECK(tensor_ty != nullptr) << "We can not set layout for non-tensor struct";
if (!tensor_ty->shape.has_value()) return ty;
const ShapeExprNode* shape = tensor_ty->shape.value().as<ShapeExprNode>();
if (shape == nullptr) return ty;
TVM_FFI_ICHECK_EQ(shape->values.size(), to.LeafValue()->layout.ndim());
std::vector<PrimExpr> new_shape;
for (size_t i = 0; i < shape->values.size(); ++i) {
new_shape.push_back(
shape->values[from.LeafValue()->layout.IndexOf(to.LeafValue()->layout[i])]);
}
VDevice vdev = tensor_ty->vdevice.value_or(VDevice());
return TensorType(ShapeExpr(new_shape), tensor_ty->dtype, vdev, tensor_ty->span);
};
Type new_ty = TransformTupleLeaf<LayoutDecision>(
binding->ty, std::array<NLayout, 2>({from_layout, input_layout}), fvisitleaf);
// re-emit old binding if nothing changes
if (new_ty.same_as(binding->ty)) {
builder_->EmitNormalized(ffi::GetRef<MatchCast>(binding));
} else {
Var new_var = builder_->EmitMatchCast(RewriteExpr(binding->value, input_layout), new_ty);
var_layout_map_[binding->var] = input_layout;
this->var_remap_[binding->var] = new_var;
}
}
std::unordered_map<Var, NLayout> var_layout_map_;
ffi::Map<ffi::String, ffi::Array<ffi::String>> desired_layouts_;
LayoutCb layout_cb_;
}; // namespace relax
DataflowBlock ConvertLayoutPass(const DataflowBlock& df_block,
ffi::Map<ffi::String, ffi::Array<ffi::String>> desired_layouts,
LayoutCb layout_cb) {
LayoutConvertMutator mutator(desired_layouts, layout_cb);
return mutator.VisitBindingBlock(df_block).as_or_throw<DataflowBlock>();
}
namespace transform {
Pass ConvertLayout(ffi::Map<ffi::String, ffi::Array<ffi::String>> desired_layouts,
LayoutCb layout_cb) {
ffi::TypedFunction<DataflowBlock(DataflowBlock, IRModule, PassContext)> pass_func =
[=](DataflowBlock df_block, IRModule m, PassContext pc) {
return ConvertLayoutPass(df_block, desired_layouts, layout_cb);
};
return CreateDataflowBlockPass(pass_func, 0, "ConvertLayout", {});
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def("relax.transform.ConvertLayout", ConvertLayout);
}
} // namespace transform
} // namespace relax
} // namespace tvm