blob: e433e9a3cc885e8a7baaf7ee7adcaffd43bc7acf [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/device_aware_visitors.cc
* \brief Visitors which track the device for the current Relay expression.
*/
#include "./device_aware_visitors.h"
namespace tvm {
namespace relay {
namespace transform {
// TODO(mbs): This machinery can be used a) on expressions/modules which have not had
// device planning run, and b) on expressions for which we've not kept track of their
// containing module. For now we'll handle b) by being forgiving as possible when recovering
// the device for an expression, and we'll support a) the same way. But better would be
// to ICHECK fail when, eg, a variable is not in scope or the lexical device stack is empty.
LexicalOnDeviceMixin::LexicalOnDeviceMixin(const Optional<IRModule>& maybe_mod) {
if (maybe_mod) {
for (const auto& kv : maybe_mod.value()->functions) {
if (const auto* function_node = kv.second.as<FunctionNode>()) {
VirtualDevice virtual_device = function_node->virtual_device();
if (!virtual_device->IsFullyUnconstrained()) {
VLOG(2) << "global '" << kv.first->name_hint << "' has virtual device " << virtual_device;
global_var_virtual_devices_.emplace(kv.first, virtual_device);
}
}
}
}
}
VirtualDevice LexicalOnDeviceMixin::GetVirtualDevice(const Expr& expr) const {
OnDeviceProps props = GetOnDeviceProps(expr);
if (props.body.defined() && props.is_fixed()) {
return props.virtual_device;
} else if (const auto* var_node = expr.as<VarNode>()) {
// Lookup variable binding.
auto itr = var_virtual_devices_.find(GetRef<Var>(var_node));
if (itr != var_virtual_devices_.end()) {
return itr->second;
}
// else: fallthrough to unconstrained
} else if (const auto* global_var_node = expr.as<GlobalVarNode>()) {
// Lookup global variable.
auto itr = global_var_virtual_devices_.find(GetRef<GlobalVar>(global_var_node));
if (itr != global_var_virtual_devices_.end()) {
return itr->second;
}
// else: fallthrough to unconstrained
} else if (const auto* function_node = expr.as<FunctionNode>()) {
if (function_node->HasNonzeroAttr(attr::kPrimitive)) {
if (!expr_virtual_devices_.empty()) {
// Use the currently in-scope device type.
return expr_virtual_devices_.back();
}
// else: fallthrough to unconstrained
} else {
return function_node->virtual_device();
}
} else {
if (!expr_virtual_devices_.empty()) {
// Use the currently in-scope device type.
return expr_virtual_devices_.back();
}
// else: fallthrough to unconstrained
}
return VirtualDevice::FullyUnconstrained();
}
void LexicalOnDeviceMixin::EnterFunctionBody() { ++function_nesting_; }
void LexicalOnDeviceMixin::ExitFunctionBody() {
ICHECK_GT(function_nesting_, 0);
--function_nesting_;
}
void LexicalOnDeviceMixin::PushVirtualDevice(const VirtualDevice& virtual_device) {
if (virtual_device->IsFullyUnconstrained()) {
return;
}
expr_virtual_devices_.emplace_back(virtual_device);
}
void LexicalOnDeviceMixin::PopVirtualDevice() {
if (expr_virtual_devices_.empty()) {
return;
}
expr_virtual_devices_.pop_back();
}
void LexicalOnDeviceMixin::PushBoundVar(Var var, const VirtualDevice& virtual_device) {
if (virtual_device->IsFullyUnconstrained()) {
return;
}
ICHECK(var_virtual_devices_.find(var) == var_virtual_devices_.end());
var_virtual_devices_.emplace(std::move(var), virtual_device);
}
void LexicalOnDeviceMixin::PopBoundVar(const Var& var) {
auto itr = var_virtual_devices_.find(var);
if (itr == var_virtual_devices_.end()) {
return;
}
var_virtual_devices_.erase(itr);
}
// TODO(mbs): We'd probably have less tedious code duplication if we redefined the memoizing
// mutator on top of the generic Functor.
void DeviceAwareExprVisitor::VisitExpr_(const FunctionNode* function_node) {
if (function_node->HasNonzeroAttr(attr::kPrimitive)) {
// No tracking inside primitive functions.
DeviceAwareVisitExpr_(function_node);
} else {
// Function parameters come into scope.
for (auto param : function_node->params) {
PushBoundVar(param, param->virtual_device());
}
// Entering scope of function body.
PushVirtualDevice(function_node->virtual_device());
EnterFunctionBody();
DeviceAwareVisitExpr_(function_node);
// Leaving scope of function body.
ExitFunctionBody();
PopVirtualDevice();
// Function parameters go out of scope.
for (size_t i = 0; i < function_node->params.size(); ++i) {
PopBoundVar(function_node->params[i]);
}
}
}
void DeviceAwareExprVisitor::VisitExpr_(const LetNode* let_node) {
PreVisitLetBlock_(let_node);
std::vector<const LetNode*> bindings;
Expr expr = GetRef<Expr>(let_node);
while (const auto* inner_let_node = expr.as<LetNode>()) {
// Let-bound var (in pre visited version) goes into scope.
// (We'll just assume this is a letrec).
PushBoundVar(inner_let_node->var, GetVirtualDevice(inner_let_node->value));
PreVisitLetBinding_(inner_let_node->var, inner_let_node->value);
bindings.emplace_back(inner_let_node);
expr = inner_let_node->body;
}
VisitExpr(expr);
for (auto itr = bindings.rbegin(); itr != bindings.rend(); ++itr) {
// Let-bound var goes out of scope.
PopBoundVar((*itr)->var);
PostVisitLet_(*itr);
}
PostVisitLetBlock_(let_node);
}
void DeviceAwareExprVisitor::VisitExpr_(const CallNode* call_node) {
OnDeviceProps props = GetOnDeviceProps(call_node);
if (props.body.defined() && props.is_fixed()) {
// Entering lexical scope of fixed "on_device" call.
PushVirtualDevice(props.virtual_device);
VisitExpr(props.body);
// Leaving lexical scope of "on_device" call.
PopVirtualDevice();
} else {
DeviceAwareVisitExpr_(call_node);
}
}
void DeviceAwareExprVisitor::DeviceAwareVisitExpr_(const FunctionNode* function_node) {
ExprVisitor::VisitExpr_(function_node);
}
void DeviceAwareExprVisitor::DeviceAwareVisitExpr_(const CallNode* call_node) {
ExprVisitor::VisitExpr_(call_node);
}
void DeviceAwareExprVisitor::PreVisitLetBlock_(const LetNode* let_node) {
// no-op
}
void DeviceAwareExprVisitor::PreVisitLetBinding_(const Var& var, const Expr& value) {
VisitExpr(var);
VisitExpr(value);
}
void DeviceAwareExprVisitor::PostVisitLet_(const LetNode* let_node) {
// no-op
}
void DeviceAwareExprVisitor::PostVisitLetBlock_(const LetNode* let_node) {
// no-op
}
Expr DeviceAwareExprMutator::VisitExpr_(const FunctionNode* function_node) {
if (function_node->HasNonzeroAttr(attr::kPrimitive)) {
// No tracking inside primitive functions.
return DeviceAwareVisitExpr_(function_node);
} else {
// Function parameters come into scope.
for (auto param : function_node->params) {
PushBoundVar(param, param->virtual_device());
}
// Entering scope of function body.
PushVirtualDevice(function_node->virtual_device());
EnterFunctionBody();
Expr result = DeviceAwareVisitExpr_(function_node);
// Leaving scope of function body.
ExitFunctionBody();
PopVirtualDevice();
// Function parameters go out of scope.
for (size_t i = 0; i < function_node->params.size(); ++i) {
PopBoundVar(function_node->params[i]);
}
return result;
}
}
Expr DeviceAwareExprMutator::VisitExpr_(const LetNode* let_node) {
PreVisitLetBlock_(let_node);
std::vector<std::tuple<Var, Expr, Span, const LetNode*>> bindings;
Expr expr = GetRef<Expr>(let_node);
while (const auto* inner_let_node = expr.as<LetNode>()) {
// Let-bound var (in pre visited version) goes into scope.
// (We'll just assume this is a letrec.)
PushBoundVar(inner_let_node->var, GetVirtualDevice(inner_let_node->value));
std::pair<Var, Expr> pair = PreVisitLetBinding_(inner_let_node->var, inner_let_node->value);
bindings.emplace_back(pair.first, pair.second, inner_let_node->span, inner_let_node);
expr = inner_let_node->body;
}
expr = VisitExpr(expr);
for (auto itr = bindings.rbegin(); itr != bindings.rend(); ++itr) {
// Let-bound var goes out of scope.
const LetNode* pre_let_node = std::get<3>(*itr);
PopBoundVar(pre_let_node->var);
Let post_let = Let(/*var=*/std::get<0>(*itr), /*value=*/std::get<1>(*itr),
/*body=*/expr, /*span=*/std::get<2>(*itr));
expr = PostVisitLet_(pre_let_node, post_let.get());
}
return PostVisitLetBlock_(let_node, expr.as<LetNode>());
}
Expr DeviceAwareExprMutator::VisitExpr_(const CallNode* call_node) {
OnDeviceProps props = GetOnDeviceProps(call_node);
if (props.body.defined() && props.is_fixed()) {
// Entering lexical scope of fixed "on_device" call.
PushVirtualDevice(props.virtual_device);
Expr expr = VisitExpr(props.body);
// Leaving lexical scope of "on_device" call.
PopVirtualDevice();
return MaybeOnDeviceWithProps(expr, props);
} else {
return DeviceAwareVisitExpr_(call_node);
}
}
Expr DeviceAwareExprMutator::DeviceAwareVisitExpr_(const FunctionNode* function_node) {
return ExprMutator::VisitExpr_(function_node);
}
Expr DeviceAwareExprMutator::DeviceAwareVisitExpr_(const CallNode* call_node) {
return ExprMutator::VisitExpr_(call_node);
}
void DeviceAwareExprMutator::PreVisitLetBlock_(const LetNode* let_node) { /* no-op */
}
std::pair<Var, Expr> DeviceAwareExprMutator::PreVisitLetBinding_(const Var& var,
const Expr& value) {
return std::make_pair(Downcast<Var>(VisitExpr(var)), VisitExpr(value));
}
Expr DeviceAwareExprMutator::PostVisitLet_(const LetNode* pre_let_node,
const LetNode* post_let_node) {
if (pre_let_node->var == post_let_node->var && pre_let_node->value == post_let_node->value &&
pre_let_node->body == post_let_node->body) {
return GetRef<Expr>(pre_let_node);
} else {
return GetRef<Expr>(post_let_node);
}
}
Expr DeviceAwareExprMutator::PostVisitLetBlock_(const LetNode* pre_let_node,
const LetNode* post_let_node) {
if (pre_let_node->var == post_let_node->var && pre_let_node->value == post_let_node->value &&
pre_let_node->body == post_let_node->body) {
return GetRef<Expr>(pre_let_node);
} else {
return GetRef<Expr>(post_let_node);
}
}
std::unordered_map<const ExprNode*, VirtualDevice> RecoverVirtualDeviceMap(const IRModule& mod,
const Expr& expr) {
class Visitor : public DeviceAwareExprVisitor {
public:
explicit Visitor(const Optional<IRModule>& maybe_mod) : DeviceAwareExprVisitor(maybe_mod) {}
void VisitExpr(const Expr& expr) final {
if (expr->IsInstance<OpNode>() || expr->IsInstance<ConstructorNode>()) {
// Don't record for ops or constructors since they are 'device polymorphic'.
} else {
map_[expr.get()] = GetVirtualDevice(expr);
}
DeviceAwareExprVisitor::VisitExpr(expr);
}
std::unordered_map<const ExprNode*, VirtualDevice> map_;
};
Visitor visitor(mod);
visitor.VisitExpr(expr);
return std::move(visitor.map_);
}
// Export the helper function for testing.
TVM_REGISTER_GLOBAL("relay.transform.RecoverVirtualDeviceMap")
.set_body_typed([](const IRModule& mod, const Expr& expr) {
std::unordered_map<const ExprNode*, VirtualDevice> raw_map =
RecoverVirtualDeviceMap(mod, expr);
Map<Expr, VirtualDevice> map;
for (const auto& kv : raw_map) {
map.Set(GetRef<Expr>(kv.first), kv.second);
}
return map;
});
} // namespace transform
} // namespace relay
} // namespace tvm