blob: 5b584e199dc7381ba6540309a03d984e817613a3 [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/relay/transforms/memory_alloc.cc
* \brief A pass for manifesting explicit memory allocations.
*/
#include <tvm/node/structural_equal.h>
#include <tvm/node/structural_hash.h>
#include <tvm/relay/analysis.h>
#include <tvm/relay/attrs/annotation.h>
#include <tvm/relay/attrs/call.h>
#include <tvm/relay/attrs/device_copy.h>
#include <tvm/relay/attrs/memory.h>
#include <tvm/relay/expr.h>
#include <tvm/relay/expr_functor.h>
#include <tvm/relay/op.h>
#include <tvm/relay/transform.h>
#include <tvm/runtime/logging.h>
#include <tvm/target/target.h>
#include <cstdint>
#include <cstdio>
#include <string>
#include <unordered_set>
#include <vector>
#include "../backend/te_compiler.h"
#include "../backend/te_compiler_cache.h"
#include "../op/annotation/annotation.h"
#include "../op/call/call.h"
#include "../op/memory/device_copy.h"
#include "../op/memory/memory.h"
#include "../op/vm/vm.h"
#include "./device_aware_visitors.h"
#include "./let_list.h"
#include "./pass_utils.h"
#include "./pattern_utils.h"
using namespace tvm::runtime;
namespace tvm {
namespace relay {
class DialectRewriter : public transform::DeviceAwareExprMutator {
public:
DialectRewriter(IRModule mod, VirtualDevice host_virtual_device)
: transform::DeviceAwareExprMutator(mod),
mod_(std::move(mod)),
host_virtual_device_(std::move(host_virtual_device)) {}
Function Rewrite(const Function& expr) { return Downcast<Function>(Mutate(expr)); }
private:
using ExprMutator::VisitExpr_;
Expr VisitExpr_(const TupleNode* tuple_node) final {
LetList& scope = scopes_.back();
Array<Expr> new_fields;
new_fields.reserve(tuple_node->fields.size());
for (auto field : tuple_node->fields) {
auto new_field = Mutate(field);
if (const auto* op = new_field.as<ConstantNode>()) {
DataType dtype(op->data->dtype);
bool is_simple_const = (dtype == DataType::Int(32) || dtype == DataType::Int(64) ||
dtype == DataType::Float(32) || dtype == DataType::Float(64) ||
dtype == DataType::Bool());
if (!op->is_scalar() || !is_simple_const) {
VirtualDevice virtual_device = GetVirtualDevice(field);
ICHECK(!virtual_device->IsFullyUnconstrained());
Var const_var("const", Type(nullptr));
new_field = scope.Push(const_var, MaybeOnDeviceFixed(new_field, virtual_device));
}
}
new_fields.push_back(new_field);
}
return WithFields(GetRef<Tuple>(tuple_node), new_fields);
}
void PreVisitLetBlock_(const LetNode* let_node) final { scopes_.emplace_back(); }
std::pair<Var, Expr> PreVisitLetBinding_(const Var& var, const Expr& value) final {
Expr new_value = Mutate(value);
VirtualDevice virtual_device = GetVirtualDevice(value);
ICHECK(!virtual_device->IsFullyUnconstrained());
scopes_.back().Push(var, MaybeOnDeviceFixed(new_value, virtual_device));
// Since we always need a let block on which to bind sub-expressions the rewritten bindings
// are tracked in the current scopes. But return the rewritten binding anyway.
return {var, new_value};
}
Expr PostVisitLetBlock_(const LetNode* pre_let_node, const LetNode* post_let_node) final {
// The current scope has captured all the rewritten let-binding, as well as any additional
// bindings we needed to add. All we need is the rewritted body.
Expr new_body = post_let_node->body;
while (const auto* inner_let_node = new_body.as<LetNode>()) {
new_body = inner_let_node->body;
}
auto ret = scopes_.back().Get(new_body);
scopes_.pop_back();
return ret;
}
Expr DeviceAwareVisitExpr_(const CallNode* call_node) final {
DeviceCopyProps device_copy_props = GetDeviceCopyProps(call_node);
CallLoweredProps call_lowered_props = GetCallLoweredProps(call_node);
if (device_copy_props.body.defined()) {
// Special case: device_copy calls remain in their original (and functional) form.
// TODO(mbs): device_copy cleanup.
return transform::DeviceAwareExprMutator::DeviceAwareVisitExpr_(call_node);
}
if (!call_lowered_props.lowered_func.defined()) {
// This is a call to a user-defined Relay functinon, which will be handled directly by
// the VM and does not need conversion to DPS.
return transform::DeviceAwareExprMutator::DeviceAwareVisitExpr_(call_node);
}
Call call = GetRef<Call>(call_node);
VLOG(1) << "converting lowered call to DPS:" << std::endl << PrettyPrint(call);
VirtualDevice virtual_device = GetVirtualDevice(call);
ICHECK(!virtual_device->IsFullyUnconstrained());
ICHECK(!scopes_.empty())
<< "Calls out of a let block are not supported, do you forget to transform "
<< "with ToANormalForm or set opt_level >= 1 in the pass context?";
LetList& scope = scopes_.back();
std::vector<Expr> new_args;
for (const auto& arg : call_lowered_props.arguments) {
new_args.push_back(Mutate(arg));
}
Tuple ins(new_args);
Type ret_type = call_node->checked_type_;
std::vector<TensorType> out_types = FlattenTupleType(ret_type);
// Handle reshape.
// Original:
// reshape(body, <ReshapeAttrs>)
// dyn.reshape(body, shape, <ReshapeAttrs>)
// After FuseOps:
// let %f = fn(x, primitive=1, relay.reshape_only=1) { reshape(x, <ReshapeAttrs>) }
// %f(body)
// After LowerTEPass:
// call_lowered(@xxx_reshape, (body), <LoweredCallAttrs with
// relay_attrs|->dict[relay.reshape_only] = 1)
// -OR-
// call_lowered(@xxx_dyn_reshape, (body, shape), <LoweredCallAttrs with same>)
// where @reshape_xxx is bound as a PrimFunc.
// (the name is irrelevant, only the relay.reshape_only attribute matters)
// After this pass:
// vm.reshape_tensor(body, shape, <TIRCallAttrs>)
if (IsReshapeOnly(call_lowered_props)) {
return EmitReshapeTensor(&scope, ins, call_lowered_props.attrs, ret_type);
}
// At this point we could be calling a PrimFunc or an 'external' and already compiled primitive.
// The calling conventions are identical.
// Handle 'dynamic' calls, ie to PrimFuncs whose result shape must be first computed
// by a companion shape function.
if (IsDynamic(ret_type)) {
return DynamicInvoke(&scope, call_lowered_props.lowered_func, ins, call_lowered_props.attrs,
out_types, ret_type, virtual_device);
}
// Handle ordinary primitive calls.
Array<Expr> outputs;
for (size_t i = 0; i < out_types.size(); ++i) {
outputs.push_back(
MakeStaticAllocation(&scope, out_types[i], virtual_device, std::to_string(i)));
}
Tuple outs(outputs);
Expr invoke =
InvokeTVMOp(call_lowered_props.lowered_func, ins, outs,
Downcast<DictAttrs>(call_lowered_props.attrs.metadata.at("relay_attrs")));
scope.Push(MaybeOnDeviceFixed(invoke, virtual_device));
return ToTupleType(ret_type, std::vector<Expr>(outputs.begin(), outputs.end()));
}
/*!
* \brief Returns the Relay Constant representing the 1d tensor with \p value.
*
* CAUTION: Make sure the constant ends up on the correct device.
*/
inline Constant MakeConstant(const std::vector<int64_t>& value) {
return MakeConstantTensor(DataType::Int(64), {static_cast<int64_t>(value.size())}, value);
}
/*! Returns an \p alloc_tensor call for a tensor of \p shape and \p dtype over \p storage. */
inline Expr AllocTensor(const Expr& storage, tvm::relay::Expr shape, DataType dtype,
Array<IndexExpr> assert_shape) {
Expr offset =
MaybeOnDeviceFixed(MakeConstantScalar(DataType::Int(64), 0), host_virtual_device_);
return tvm::relay::AllocTensor(storage, std::move(offset), std::move(shape), dtype,
assert_shape);
}
Expr ComputeAlignment(const DataType& dtype) const {
int64_t align = dtype.bits() / 8 * dtype.lanes();
if (align < 64) {
align = 64;
}
return MakeConstantScalar(DataType::Int(64), align);
}
Expr ComputeStorageInRelay(const Expr& shape, const TensorType& type) const {
auto dtype = DataType(type->dtype);
Expr els = Prod(shape, Array<Integer>(nullptr), false, false);
Expr num = MakeConstantScalar(DataType::Int(64), dtype.bits() * dtype.lanes());
Expr add = Add(num, MakeConstantScalar(DataType::Int(64), 7));
Expr div = MakeConstantScalar(DataType::Int(64), 8);
Expr ret = Multiply(els, Divide(add, div));
return std::move(ret);
}
Expr ComputeStorage(const TensorType& type) {
int64_t size = 1;
for (auto it : type->shape) {
auto val = it.as<IntImmNode>();
CHECK(val);
size *= val->value;
}
size *= (type->dtype.bits() * type->dtype.lanes() + 7) / 8;
return std::move(MakeConstantScalar(DataType::Int(64), size));
}
// Allocate a tensor with a statically known shape.
Var MakeStaticAllocation(LetList* scope, const TensorType& type,
const VirtualDevice& virtual_device, String name_hint) {
std::vector<int64_t> int_shape;
for (auto it : type->shape) {
const auto* imm = it.as<IntImmNode>();
CHECK(imm) << "expect static int shape";
int_shape.push_back(imm->value);
}
Expr shape = MaybeOnDeviceFixed(MakeConstant(int_shape), host_virtual_device_);
Expr size = MaybeOnDeviceFixed(ComputeStorage(type), host_virtual_device_);
// Alignment is directly captured in the instruction rather than calculated, so we
// don't want to wrap it with an "on_device".
Expr alignment = ComputeAlignment(type->dtype);
// Run type inference later to get the correct type.
Var var("storage_" + name_hint, Type(nullptr));
Expr value = AllocStorage(size, alignment, virtual_device, type->dtype);
auto sto = scope->Push(var, MaybeOnDeviceFixed(value, virtual_device));
// TODO(@jroesch): There is a bug with typing based on the constant shape.
auto tensor = AllocTensor(sto, shape, type->dtype, /*assert_shape=*/type->shape);
Var tensor_var("tensor_" + name_hint, Type(nullptr));
return scope->Push(tensor_var, MaybeOnDeviceFixed(tensor, virtual_device));
}
/*!
* \brief Appends to \p scope the computation necessary to call the shape function given
* in \p tir_call_attrs and bind the resulting result shapes into \p scope. The result
* shapes are for a call to a primitive with \p ins arguments. Some combinationn of the
* data and/or shapes of \p ins will be needed by the shape function.
*/
Array<Expr> EmitShapeFunc(LetList* scope, const Tuple& ins, const CallLoweredAttrs& attrs) {
ICHECK(attrs.metadata.count("prim_shape_fn_states"));
Array<Integer> input_states =
Downcast<Array<Integer>>(attrs.metadata.at("prim_shape_fn_states"));
ICHECK(attrs.metadata.count("prim_shape_fn_var"));
auto prim_fn_var = Downcast<GlobalVar>(attrs.metadata.at("prim_shape_fn_var"));
const auto* func_type_node = prim_fn_var->checked_type().as<FuncTypeNode>();
ICHECK(func_type_node);
// Establish the arguments to the shape function.
Array<Expr> shape_func_ins;
int input_pos = 0;
ICHECK_EQ(ins->fields.size(), input_states.size());
for (size_t i = 0; i < ins->fields.size(); ++i) {
const Expr& arg = ins->fields[i];
Type ty;
if (const auto* vn = arg.as<VarNode>()) {
ty = vn->type_annotation;
} else {
ty = arg->checked_type();
}
int64_t state = input_states[i]->value;
// Pass Shapes
if (state == tec::kNeedInputShape) {
std::vector<Expr> exprs = FromTupleType(ty, arg);
for (size_t j = 0; j < exprs.size(); ++j) {
Expr sh_of = Mutate(ShapeOf(exprs[j]));
Var in_shape_var("in_shape_" + std::to_string(input_pos + j), Type(nullptr));
shape_func_ins.push_back(
scope->Push(in_shape_var, MaybeOnDeviceFixed(sh_of, host_virtual_device_)));
input_pos++;
}
} else if (state == tec::kNeedInputData) {
auto new_arg = Mutate(arg); // already accounts for device
VirtualDevice arg_virtual_device = GetVirtualDevice(arg);
ICHECK(!arg_virtual_device->IsFullyUnconstrained());
// The dynamic shape function is expecting its data on the host/CPU, so insert a
// device_copy otherwise. (We'll need to fuse & lower these copies in the same way
// we fuse & lower other operators we insert for, eg, dynamic tensor size calculation.)
new_arg = MaybeDeviceCopy(MaybeOnDeviceFixed(new_arg, arg_virtual_device),
arg_virtual_device, host_virtual_device_);
Var in_shape_var("in_shape_" + std::to_string(input_pos), Type(nullptr));
shape_func_ins.push_back(
scope->Push(in_shape_var, MaybeOnDeviceFixed(new_arg, host_virtual_device_)));
input_pos++;
} else {
// TODO(@jroesch): handle kNeedBoth
LOG(FATAL) << "unsupported shape function input state";
}
}
ICHECK_EQ(shape_func_ins.size(), func_type_node->arg_types.size());
// Establish the result shapes.
const auto* res_tuple_node = func_type_node->ret_type.as<TupleTypeNode>();
ICHECK(res_tuple_node);
Array<Expr> out_shapes;
for (size_t i = 0; i < res_tuple_node->fields.size(); ++i) {
const auto* tensor_type_node = res_tuple_node->fields[i].as<TensorTypeNode>();
ICHECK(tensor_type_node);
// Put the shape func on the host. This also ensures that everything between
// shape_of and shape_func is similarly on the host.
Var alloc = MakeStaticAllocation(scope, GetRef<TensorType>(tensor_type_node),
host_virtual_device_, "out_shape_" + std::to_string(i));
out_shapes.push_back(alloc);
}
// Represent the call in DPS form.
auto shape_call = InvokeTVMOp(prim_fn_var, Tuple(shape_func_ins), Tuple(out_shapes),
Downcast<DictAttrs>(attrs.metadata.at("relay_attrs")));
Var shape_func_var("shape_func", Type(nullptr));
scope->Push(shape_func_var, MaybeOnDeviceFixed(shape_call, host_virtual_device_));
return out_shapes;
}
// Generate the code for invoking the TVM primitive \p func who's results have dynamic shapes.
Expr DynamicInvoke(LetList* scope, const Expr& func, const Tuple& ins,
const CallLoweredAttrs& attrs, const std::vector<TensorType>& out_types,
const Type& ret_type, const VirtualDevice& virtual_device) {
Array<Expr> out_shapes = EmitShapeFunc(scope, ins, attrs);
std::vector<Var> storages;
CHECK_EQ(out_shapes.size(), out_types.size());
for (size_t i = 0; i < out_shapes.size(); ++i) {
auto out_shape = out_shapes[i];
auto out_type = out_types[i];
auto size =
MaybeOnDeviceFixed(ComputeStorageInRelay(out_shape, out_type), host_virtual_device_);
// Alignment is directly captured in the instruction so don't wrap in "on_device".
auto alignment = ComputeAlignment(out_type->dtype);
Var sto_var("storage_" + std::to_string(i), Type(nullptr));
auto val = AllocStorage(size, alignment, virtual_device, out_type->dtype);
storages.push_back(scope->Push(sto_var, MaybeOnDeviceFixed(val, virtual_device)));
}
Array<Expr> outs;
for (size_t i = 0; i < storages.size(); ++i) {
auto out_shape = out_shapes[i];
auto out_type = out_types[i];
auto storage = storages[i];
auto alloc = AllocTensor(storage, out_shape, out_type->dtype, out_type->shape);
Var out_var("out_" + std::to_string(i), Type(nullptr));
outs.push_back(scope->Push(out_var, MaybeOnDeviceFixed(alloc, virtual_device)));
}
Tuple tuple_outs(outs);
auto call =
InvokeTVMOp(func, ins, tuple_outs, Downcast<DictAttrs>(attrs.metadata.at("relay_attrs")));
scope->Push(MaybeOnDeviceFixed(call, virtual_device));
return ToTupleType(ret_type,
std::vector<Expr>(tuple_outs->fields.begin(), tuple_outs->fields.end()));
}
Expr EmitReshapeTensor(LetList* scope, const Tuple& ins, const CallLoweredAttrs& attrs,
const Type& ret_type) {
ICHECK_GE(ins->fields.size(), 1); // static reshape
ICHECK_LE(ins->fields.size(), 2); // dynamic reshape, second arg is shape
TensorType ret_ty = Downcast<TensorType>(ret_type);
Expr shape_expr;
if (IsDynamic(ret_type)) {
// Even though the desired output shape has been passed as the second argument to
// the dyn.reshape primitive, we'll still call that primitive's shape function. Go figure.
Array<Expr> out_shapes = EmitShapeFunc(scope, ins, attrs);
ICHECK_EQ(out_shapes.size(), 1);
shape_expr = out_shapes[0];
} else {
std::vector<int64_t> shape;
for (const auto& it : ret_ty->shape) {
const auto* imm = it.as<IntImmNode>();
CHECK(imm) << "expect static int shape";
shape.push_back(imm->value);
}
shape_expr = MaybeOnDeviceFixed(MakeConstant(shape), host_virtual_device_);
}
return ReshapeTensor(ins->fields[0], shape_expr, ret_ty->shape);
}
private:
const Op& device_copy_op_ = Op::Get("device_copy");
runtime::DataType compute_dtype_ = runtime::DataType::Int(64);
IRModule mod_;
VirtualDevice host_virtual_device_;
std::vector<LetList> scopes_;
};
namespace transform {
Pass ManifestAllocImportStorage() {
auto pass_func = [](IRModule mod, tvm::transform::PassContext pass_cnxt) {
mod.CopyOnWrite();
mod->ImportFromStd("core.rly");
return mod;
};
return tvm::transform::CreateModulePass(pass_func, /*opt_level=*/0, "ManifestAllocImportStorage",
/*required=*/{});
}
Pass ManifestAllocImpl(VirtualDevice host_virtual_device) {
auto pass_func = [host_virtual_device](Function func, IRModule mod, PassContext ctxt) {
return DialectRewriter(mod, host_virtual_device).Rewrite(func);
};
return CreateFunctionPass(pass_func, 0, "ManifestAllocImpl", {});
}
Pass ManifestAlloc(VirtualDevice cpu_virtual_device) {
std::vector<Pass> passes = {ManifestAllocImportStorage(), InferType(),
ManifestAllocImpl(std::move(cpu_virtual_device)), InferType()};
return Sequential(passes, "ManifestAlloc");
}
TVM_REGISTER_GLOBAL("relay.transform.ManifestAlloc").set_body_typed(ManifestAlloc);
} // namespace transform
} // namespace relay
} // namespace tvm