blob: c9cf45e06929a79ca006a47b65bbae65eb46e4df [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 annotate_texture_storage.cc
* \brief Collection of target specific relay passes which
* storage scope related information.
*
* - CollectStorageInfo returns a mapping from relay expr
* to a map of storage scopes for each call argument.
* These scopes are used during memory planning as well
* as downstream when doing codegen and in the graph runtime when doing runtime dataspace
* allocations.
*
* - AnnotateMemoryScope calls *target.CollectStorageInfo for all target been represented
* in the graph and rewrites graph modifying or inserting of VirtualDevice with required
* memory_scope collected from the CollectStorageInfo
*/
#include <tvm/relay/attrs/nn.h>
#include <tvm/relay/expr.h>
#include <tvm/relay/expr_functor.h>
#include <tvm/relay/transform.h>
#include <tvm/tir/expr.h>
#include <memory>
#include <unordered_map>
#include "../op/memory/device_copy.h"
#include "../op/memory/memory.h"
#include "../transforms/device_aware_visitors.h"
namespace tvm {
namespace relay {
namespace {
/**
* @brief Analyzes the graph and returns mapping of expressions vs desired memory scope
*/
class StorageInfo : private transform::DeviceAwareExprVisitor {
public:
StorageInfo() : transform::DeviceAwareExprVisitor(Optional<IRModule>()) {}
static Map<Expr, Map<Expr, Array<String>>> GetStorageMap(const Expr& expr) {
StorageInfo storage_info;
storage_info.VisitExpr(expr);
storage_info.LegalizeProducerStorage();
Map<Expr, Map<Expr, Array<String>>> storage_map = storage_info.accept_textures_;
for (auto& kv : storage_info.storage_scope_) {
std::vector<String> storage_scopes;
std::copy(kv.second.begin(), kv.second.end(), std::back_inserter(storage_scopes));
Map<Expr, Array<String>> ent;
ent.Set(Expr(), Array<String>{storage_scopes});
storage_map.Set(GetRef<Expr>(kv.first), ent);
}
// Filling the input arguments by "global" scope to handle PlanDevice algo which propagates
// virtual devices from outputs to inputs. At the same time outputs must be unconstrained
// to avoid useless device_copy
for (const auto& cs : storage_info.consumer_storage_scopes_) {
// we have record in consumers that mean that potentially consumer
// dealt with textures anyhow, it's safe to mark this expr as global scope
// even without verification of the consumer's outputs scope
if (storage_info.CanConsumeTextures(cs.second) &&
storage_map.find(GetRef<Expr>(cs.first)) == storage_map.end()) {
Map<Expr, Array<String>> ent;
ent.Set(Expr(), Array<String>{"global"});
storage_map.Set(GetRef<Expr>(cs.first), ent);
}
}
// initial algo assumes mapping of outputs of the expr that is not enough, need to update
// VirtualDevice for function variables to get proper codegen. Adding vars to storage_map
for (const auto& a : storage_info.args_to_vars_) {
if (storage_map.count(a.first)) {
for (const auto& v : a.second) {
storage_map.Set(v, storage_map[a.first]);
if (storage_map[a.first][Expr()][0] == "global" &&
storage_info.accept_textures_.count(v)) {
Map<Expr, Array<String>> ent;
ent.Set(Expr(), storage_info.accept_textures_[v][Expr()]);
storage_map.Set(v, ent);
for (const auto& calls : storage_info.accept_textures_[v]) {
if (calls.first != Expr()) {
if (storage_map.count(a.first)) {
Map<Expr, Array<String>> ent_call = storage_map[a.first];
ent_call.Set(calls.first, calls.second);
storage_map.Set(a.first, ent_call);
} else {
Map<Expr, Array<String>> ent_call;
ent_call.Set(calls.first, calls.second);
storage_map.Set(a.first, ent_call);
}
}
}
}
}
}
}
return storage_map;
}
private:
using transform::DeviceAwareExprVisitor::VisitExpr_;
void Visit(const Expr& expr) {
// Pre-order traversal to enable upward propagation
// of consumer storage scopes to producers when desirable.
if (const auto* fn = expr.as<FunctionNode>()) {
this->VisitExpr(fn->body);
for (const auto& param : fn->params) {
this->VisitExpr(param);
}
} else {
this->VisitExpr(expr);
}
}
void VisitExpr_(const VarNode* vn) final { ApplyConsumerScopeToInputs(vn); }
void VisitExpr_(const ConstantNode* cn) final { ApplyConsumerScopeToInputs(cn); }
void DeviceAwareVisitExpr_(const FunctionNode* function_node) final {
if (!function_node->HasNonzeroAttr(attr::kPrimitive)) {
for (auto&& param : function_node->params) {
auto virtual_device = GetVirtualDevice(param);
param->virtual_device_ =
VirtualDevice(virtual_device->device_type(), virtual_device->virtual_device_id,
virtual_device->target, "global");
}
}
transform::DeviceAwareExprVisitor::DeviceAwareVisitExpr_(function_node);
}
void DeviceAwareVisitExpr_(const CallNode* call) final {
// Check the contents of this primitive function
if (const auto* fn = call->op.as<FunctionNode>()) {
if (fn->HasNonzeroAttr(attr::kPrimitive)) {
primitive_supports_texture_ = false;
Visit(call->op);
if (primitive_supports_texture_) {
if (call->checked_type().as<TensorTypeNode>()) {
std::string scope = "global.texture";
if (const auto* ttype = call->checked_type().as<TensorTypeNode>()) {
scope = Scope(ttype->shape, GetVirtualDevice(GetRef<Expr>(call)));
}
storage_scope_[call].push_back(scope);
} else {
const auto* tuple_type = call->type_as<TupleTypeNode>();
ICHECK(tuple_type);
// TODO(csullivan): Add support for mixed output storage scope.
// In current adreno storage planner all outputs of a
// primitive function are assumed to be of the same storage
// type. This should be easy to extend in the future.
for (size_t i = 0; i < tuple_type->fields.size(); i++) {
storage_scope_[call].push_back("global.texture");
}
}
for (size_t i = 0; i < fn->params.size(); i++) {
args_to_vars_[call->args[i]].push_back(fn->params[i]);
// adding info about arguments if they can be converted to texture
for (const auto& ttype : FlattenTupleType(fn->params[i]->checked_type())) {
std::string scope = Scope(ttype->shape, GetVirtualDevice(GetRef<Expr>(call)));
if (scope.find("global.texture") != std::string::npos) {
if (accept_textures_.count(fn->params[i])) {
Map<Expr, Array<String>> ent = accept_textures_[fn->params[i]];
ent.Set(GetRef<Expr>(call), Array<String>{scope});
ent.Set(Expr(), Array<String>{scope});
accept_textures_.Set(fn->params[i], ent);
} else {
Map<Expr, Array<String>> ent;
ent.Set(GetRef<Expr>(call), Array<String>{scope});
ent.Set(Expr(), Array<String>{scope});
accept_textures_.Set(fn->params[i], ent);
}
}
}
}
}
// Add consumer storage scope information for call arguments
for (auto& arg : call->args) {
if (storage_scope_.count(call)) {
ICHECK(!HasMixedStorageOutputs(call))
<< "Mixed output storage scopes are not currently supported";
consumer_storage_scopes_[arg.operator->()].push_back("global.texture");
} else {
consumer_storage_scopes_[arg.operator->()].push_back("global");
}
}
}
}
primitive_supports_texture_ = SupportsTextureStorage(call);
for (auto& arg : call->args) {
Visit(arg);
}
// We have all callees filled into storage_scope_ if they support textures
// We need to verify if this call expects texture and if it does not, remove from
// storage_scope_ since initially storage_scope_ is filled only based on knowledge
// that function able to work with textures, but not necessary that this texture is
// expected by function callee
for (auto& arg : call->args) {
if (consumer_storage_scopes_.count(arg.operator->()) &&
GetConsumerScope(consumer_storage_scopes_[arg.operator->()]) != "global.texture") {
storage_scope_.erase(arg.operator->());
}
}
}
/**
* Defines the name of the memory scope which can fit the tensor of required shape
*
* The scope stands for "global" if tensor does not satisfy current flattening rules for textures
* (texture currently has to be 5d tensors with value eq 4 in the last dimension)
*
* The packing layout inside the texture scope (the part after the dash) is defined
* during the shape itself. Hardware can have limitations on the texture spatial dimensions
* we must not exceed these sizes. In addition to the fitting of h/w limitation we want to
* get balanced packing where final spatial sizes of textures will not be too different
* @param shape shape to be analyzed
* @param vd VirtualDevice for the tensors determined of memory scope
* @return string representing memory scope either "global" or "global.texture-layout"
*/
std::string Scope(Array<PrimExpr> shape, const VirtualDevice& vd) {
// currently we support only textures been made from 5d tensors
// 5d requirement is not limitation of textures in general, it is limitation how
// we are representing memory scopes/layout and flattening of textures in tir
if (vd != VirtualDevice::FullyUnconstrained() && shape.size() == 5 &&
shape[4].as<IntImmNode>()->value == 4) {
std::map<int, std::string> diffs;
int limit =
vd->target->GetAttr<Integer>("texture_spatial_limit").value_or(Integer(16384))->value;
int a0 = shape[0].as<IntImmNode>()->value;
int a1 = shape[1].as<IntImmNode>()->value;
int a2 = shape[2].as<IntImmNode>()->value;
int a3 = shape[3].as<IntImmNode>()->value;
int d3l = a0 * a1 * a2;
int d3r = a3;
int diff3 = d3l > d3r ? d3l - d3r : d3r - d3l;
if (d3l < limit && d3r < limit) diffs[diff3] = "";
int d2l = a0 * a1;
int d2r = a2 * a3;
int diff2 = d2l > d2r ? d2l - d2r : d2r - d2l;
if (d2l < limit && d2r < limit) diffs[diff2] = "nhwc";
int d1l = a0;
int d1r = a1 * a2 * a3;
int diff1 = d1l > d1r ? d1l - d1r : d1r - d1l;
if (d1l < limit && d1r < limit) diffs[diff1] = "weight";
if (!diffs.empty()) {
std::string scope = "global.texture";
if (!diffs.begin()->second.empty()) {
scope += ("-" + diffs.begin()->second);
}
return scope;
}
}
return "global";
}
void ApplyConsumerScopeToInputs(const ExprNode* expr) {
std::string scope;
auto consumer_scopes_it = consumer_storage_scopes_.find(expr);
if (consumer_scopes_it != consumer_storage_scopes_.end()) {
std::string consumer_scope = GetConsumerScope(consumer_scopes_it->second);
ICHECK(!storage_scope_.count(expr))
<< "Already propagated consumer scopes to input: " << GetRef<Expr>(expr);
bool expr_is_rgba_vectorizable = false;
if (const auto* ttype = expr->checked_type().as<TensorTypeNode>()) {
scope = Scope(ttype->shape, GetVirtualDevice(GetRef<Expr>(expr)));
if (scope != "global") {
auto inner_dim = ttype->shape.back().as<IntImmNode>();
if (inner_dim && inner_dim->value == 4) {
expr_is_rgba_vectorizable = true;
}
}
}
// Only propagate texture scope from consumers to input expr if
// the input shape of the input expr is rgba vectorizable.
if (consumer_scope.find("global.texture") != std::string::npos) {
if (expr_is_rgba_vectorizable) {
storage_scope_[expr].push_back(scope);
}
} else {
storage_scope_[expr].push_back(consumer_scope);
}
}
}
void LegalizeProducerStorage() {
for (auto& kv : consumer_storage_scopes_) {
const ExprNode* producer = kv.first;
std::string legal_scope = GetConsumerScope(kv.second);
if (storage_scope_.count(producer)) {
ICHECK(!HasMixedStorageOutputs(producer))
<< "Mixed output storage scopes are not currently supported";
if (storage_scope_[producer][0].find(legal_scope) == std::string::npos) {
for (size_t i = 0; i < storage_scope_[producer].size(); i++) {
// Only support uniform storage scope across all outputs for now
storage_scope_[producer][i] = legal_scope;
}
}
}
}
}
std::string GetConsumerScope(const std::vector<std::string>& consumer_scopes) const {
if (!consumer_scopes.size()) {
return "global";
}
std::string texture_tag = "global.texture";
for (auto& consumer_scope : consumer_scopes) {
if (consumer_scope.find(texture_tag) == std::string::npos) {
return "global";
}
}
return texture_tag;
}
bool CanConsumeTextures(const std::vector<std::string>& consumer_scopes) const {
std::string texture_tag = "global.texture";
for (auto& consumer_scope : consumer_scopes) {
if (consumer_scope.find(texture_tag) == 0) {
return true;
}
}
return false;
}
bool HasMixedStorageOutputs(const ExprNode* expr) {
if (storage_scope_.count(expr)) {
std::string ref_scope = storage_scope_[expr][0];
for (std::string& scope : storage_scope_[expr]) {
if (scope != ref_scope) {
return true;
}
}
}
return false;
}
bool SupportsTextureStorage(const CallNode* call) const {
bool supports_texture_storage = false;
if (auto attrs = call->attrs.as<Conv2DAttrs>()) {
if (attrs->data_layout == "NCHW4c" && attrs->kernel_layout == "OIHW4o") {
supports_texture_storage = true;
} else if (attrs->data_layout == "NHWC4c" &&
(attrs->kernel_layout == "HWOI4o" || attrs->kernel_layout == "HWIO4o" ||
attrs->kernel_layout == "OIHW4o")) {
supports_texture_storage = true;
}
} else if (auto attrs = call->attrs.as<Conv2DWinogradAttrs>()) {
if ((attrs->data_layout == "NCHW4c" || attrs->data_layout == "NHWC4c") &&
(attrs->kernel_layout == "OIHW4o" || attrs->kernel_layout == "HWIO4o")) {
supports_texture_storage = true;
}
} else if (auto attrs = call->attrs.as<GlobalPool2DAttrs>()) {
if (attrs->layout == "NCHW4c") {
supports_texture_storage = true;
}
} else if (auto attrs = call->attrs.as<MaxPool2DAttrs>()) {
if (attrs->layout == "NCHW4c") {
supports_texture_storage = true;
}
} else if (auto attrs = call->attrs.as<AvgPool2DAttrs>()) {
if (attrs->layout == "NCHW4c") {
supports_texture_storage = true;
}
} else if (const OpNode* opnode = call->op.as<OpNode>()) {
auto fpattern = Op::GetAttrMap<TOpPattern>("TOpPattern");
auto pattern = fpattern[GetRef<Op>(opnode)];
if (pattern <= kInjective) {
if (const auto* ttype = call->checked_type().as<TensorTypeNode>()) {
if (ttype->shape.size() == 5) {
supports_texture_storage = true;
}
}
}
}
return supports_texture_storage;
}
/*! \brief Temporary state for marking whether a visited function
* primitive supports texture storage scope */
bool primitive_supports_texture_ = false;
/*! \brief expr storage scope mapping for each output */
std::unordered_map<const ExprNode*, std::vector<std::string>> storage_scope_;
/*! \brief output storage scopes used by consumers of expr key */
std::unordered_map<const ExprNode*, std::vector<std::string>> consumer_storage_scopes_;
/*! \brief mapping of arguments to call to function variables*/
std::unordered_map<Expr, std::vector<Var>, ObjectPtrHash, ObjectPtrEqual> args_to_vars_;
/*! \brief mapping of arguments that can be converted to texture*/
Map<Expr, Map<Expr, Array<String>>> accept_textures_;
};
} // namespace
/**
* @brief rewrite of virtual devices, memory_scope part for expressions defined
* by the StorageInfo analysis pass
*
* Currently this workflow supports analysis and rewriting of VirtualDevice for
* Constants and function Variables
*/
class RewriteVDStorageScopes : public transform::DeviceAwareExprMutator {
using VarMap = std::unordered_map<Expr, Var, ObjectPtrHash, ObjectPtrEqual>;
public:
using transform::DeviceAwareExprMutator::VisitExpr_;
explicit RewriteVDStorageScopes(const Map<Expr, Map<Expr, Array<String>>>& storage_scope)
: transform::DeviceAwareExprMutator(Optional<IRModule>()), storage_scope_(storage_scope) {}
Function Rewrite(const Expr& expr) { return Downcast<Function>(Mutate(expr)); }
Expr VisitExpr_(const VarNode* vn) final {
if (storage_scope_.find(GetRef<Expr>(vn)) != storage_scope_.end() &&
storage_scope_[GetRef<Expr>(vn)].find(Expr()) != storage_scope_[GetRef<Expr>(vn)].end() &&
storage_scope_[GetRef<Expr>(vn)][Expr()][0] != "global") {
Var c = Var(vn->vid, vn->type_annotation, vn->span);
auto virtual_device = GetVirtualDevice(GetRef<Expr>(vn));
c->virtual_device_ =
VirtualDevice(virtual_device->device_type(), virtual_device->virtual_device_id,
virtual_device->target, storage_scope_[GetRef<Expr>(vn)][Expr()][0]);
return c;
}
return GetRef<Var>(vn);
}
Expr VisitExpr_(const ConstantNode* vn) final {
if (storage_scope_.find(GetRef<Expr>(vn)) != storage_scope_.end() &&
storage_scope_[GetRef<Expr>(vn)].find(Expr()) != storage_scope_[GetRef<Expr>(vn)].end()) {
Expr c = Constant(vn->data, vn->span);
auto virtual_device = GetVirtualDevice(GetRef<Expr>(vn));
c = OnDevice(
c,
VirtualDevice(virtual_device->device_type(), virtual_device->virtual_device_id,
virtual_device->target, storage_scope_[GetRef<Expr>(vn)][Expr()][0]),
true);
return c;
}
return GetRef<Constant>(vn);
}
Expr DeviceAwareVisitExpr_(const CallNode* call_node) final {
// we need to duplicate ExprMutator::VisitExpr_ to correct argument scopes and
// put device_copy
auto new_op = this->Mutate(call_node->op);
tvm::Array<Type> ty_args;
ty_args.reserve(call_node->type_args.size());
for (auto ty_arg : call_node->type_args) {
auto new_ty_arg = this->VisitType(ty_arg);
ty_args.push_back(new_ty_arg);
}
tvm::Array<Expr> call_args;
call_args.reserve(call_node->args.size());
for (auto arg : call_node->args) {
auto new_arg = this->Mutate(arg);
// verification if we need to put device_copy
if (storage_scope_.count(arg) && storage_scope_[arg].count(GetRef<Expr>(call_node))) {
auto virtual_device = GetVirtualDevice(GetRef<Expr>(call_node));
VirtualDevice virtual_device_from =
VirtualDevice(virtual_device->device_type(), virtual_device->virtual_device_id,
virtual_device->target, virtual_device->memory_scope);
VirtualDevice virtual_device_to =
VirtualDevice(virtual_device->device_type(), virtual_device->virtual_device_id,
virtual_device->target, storage_scope_[arg][GetRef<Expr>(call_node)][0]);
new_arg = DeviceCopy(new_arg, virtual_device_from, virtual_device_to);
new_arg = OnDevice(
new_arg,
VirtualDevice(virtual_device->device_type(), virtual_device->virtual_device_id,
virtual_device->target, storage_scope_[arg][GetRef<Expr>(call_node)][0]),
true);
}
call_args.push_back(new_arg);
}
auto new_call = WithFields(GetRef<Call>(call_node), new_op, call_args, {}, ty_args);
auto virtual_device = GetVirtualDevice(GetRef<Expr>(call_node));
std::string memory_scope = "";
if (storage_scope_.find(GetRef<Expr>(call_node)) != storage_scope_.end() &&
storage_scope_[GetRef<Expr>(call_node)].find(Expr()) !=
storage_scope_[GetRef<Expr>(call_node)].end()) {
memory_scope = storage_scope_[GetRef<Expr>(call_node)][Expr()][0];
} else if (virtual_device->memory_scope != "") {
memory_scope = virtual_device->memory_scope;
} else if (!call_node->op.as<FunctionNode>()) {
memory_scope = "";
}
if (!memory_scope.empty()) {
new_call =
OnDevice(new_call,
VirtualDevice(virtual_device->device_type(), virtual_device->virtual_device_id,
virtual_device->target, memory_scope),
true);
}
return new_call;
}
private:
Map<Expr, Map<Expr, Array<String>>> storage_scope_;
VarMap new_vars_;
Array<String> current_function_scope_;
};
Map<Expr, Map<Expr, Array<String>>> CollectTextureStorage(const Expr& expr) {
return StorageInfo::GetStorageMap(expr);
}
/**
* @brief Collects all target devices participated in graph
*/
class CollectVirtualDevices : public transform::DeviceAwareExprVisitor {
public:
CollectVirtualDevices() : transform::DeviceAwareExprVisitor(Optional<IRModule>()) {}
/**
* @brief Get all unique device elements from target of each VirtualDevice
*
* @param expr - IR
* @return set of devices
*/
std::set<std::string> GetDevices(const Expr& expr) {
this->Run(expr);
return std::move(devices_);
}
void Visit(const Expr& expr) {
// Pre-order traversal to enable upward propagation
// of consumer storage scopes to producers when desirable.
if (const auto* fn = expr.as<FunctionNode>()) {
this->VisitExpr(fn->body);
for (const auto& param : fn->params) {
this->VisitExpr(param);
}
} else {
this->VisitExpr(expr);
}
}
void DeviceAwareVisitExpr_(const CallNode* call) final {
auto vd = GetVirtualDevice(GetRef<Expr>(call));
if (vd != VirtualDevice::FullyUnconstrained()) {
if (Optional<String> t_device = vd->target->GetAttr<String>("device")) {
devices_.insert(vd->target->kind->name + "." + t_device.value());
}
}
for (auto& arg : call->args) {
Visit(arg);
}
}
void Run(const Expr& expr) { VisitExpr(expr); }
using transform::DeviceAwareExprVisitor::VisitExpr_;
std::set<std::string> devices_;
};
/*!
* \brief Collect the target specific tensor storage info for each expression's output.
* \param expr The expression.
* \return The device based storage mapping.
*/
Map<Expr, Map<Expr, Array<String>>> CollectStorageInfo(const Expr& expr) {
std::set<std::string> device_types = CollectVirtualDevices().GetDevices(expr);
// TODO(amalyshe): current approach collects all targets withing graph and call the only
// function corresponding to all these targets in alphabetic order
// this will work reliable only for case of only one device and should be redesigned
// to handle common case
std::string ftarget_prefix = "relay.backend";
for (auto& dev_id : device_types) {
ftarget_prefix += (std::string(".") + dev_id);
}
Map<Expr, Map<Expr, Array<String>>> storage_info = {};
if (const auto* f = runtime::Registry::Get(ftarget_prefix + "._CollectStorageInfo")) {
storage_info = (*f)(expr);
}
return storage_info;
}
Expr AnnotateMemoryScopeExpr(const Expr& expr, const IRModule& mod, CompilationConfig config) {
auto storage_scope = CollectStorageInfo(expr);
if (storage_scope.size()) {
return RewriteVDStorageScopes(storage_scope).Rewrite(expr);
} else {
return expr;
}
}
namespace transform {
tvm::transform::Pass AnnotateMemoryScope(CompilationConfig config) {
runtime::TypedPackedFunc<Function(Function, IRModule, PassContext)> pass_func =
[config = std::move(config)](Function f, IRModule m, PassContext pc) {
return Downcast<Function>(AnnotateMemoryScopeExpr(f, m, config));
};
return CreateFunctionPass(pass_func, 2, "AnnotateMemoryScope", {});
}
} // namespace transform
TVM_REGISTER_GLOBAL("relay.backend.opencl.adreno._CollectStorageInfo")
.set_body_typed(CollectTextureStorage);
} // namespace relay
} // namespace tvm