blob: c891e76d8b1d346d6366acdbcb0f4c158c101569 [file] [log] [blame]
/*
* Copyright (c) 2022 Arm Limited.
*
* SPDX-License-Identifier: MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#ifndef SRC_DYNAMIC_FUSION_SKETCH_UTILS_DEPENDENCYGRAPH
#define SRC_DYNAMIC_FUSION_SKETCH_UTILS_DEPENDENCYGRAPH
#include "arm_compute/core/Error.h"
#include <cstdint>
#include <map>
#include <set>
#include <tuple>
#include <vector>
namespace arm_compute
{
namespace experimental
{
namespace dynamic_fusion
{
namespace
{
template <typename T>
bool is_in(const T &v, const std::vector<T> &vec)
{
return std::find(std::begin(vec), std::end(vec), v) != std::end(vec);
}
} // namespace
/** A multi-input (tensors), multi-output (tensors) acyclic directed graph
* Represented as a doubly-linked adjacency list with the differentiation between source and destination
*/
class DependencyGraph
{
public:
using Id = int32_t;
using TensorId = Id;
using OperatorId = Id;
/** Adjacency list
*
*/
using AdjList = std::map<Id, std::vector<Id>>;
/** A pack of operator including its input and output tensors, used by traversing through the graph in topological order
*
*/
struct OpPack
{
OperatorId op{};
std::vector<TensorId> inputs{};
std::vector<TensorId> outputs{};
friend bool operator==(const OpPack &opp0, const OpPack &opp1)
{
return std::make_tuple(
opp0.op, opp0.inputs, opp0.outputs)
== std::make_tuple(
opp1.op, opp1.inputs, opp1.outputs);
}
};
public:
DependencyGraph() = default;
friend std::ostream &operator<<(std::ostream &os, const DependencyGraph &);
/** Try adding an operator (without actually adding it), while keeping the graph as a "linear sequence" / list
*
* Rule: If the new operator is not the first operator, at least one input tensor must be
* the output tensor of the last non-output operator. All other input tensors must be
* the global input of the graph (i.e. not the output of any operator).
*
* Rule: The output tensor of the new operator must not be the input tensor of any previously
* added operator.
*
* PRECONDITION: The current graph is already linear
*
* @return true If the operator can be added while keeping the graph as a linear sequence
* @return false Otherwise
*/
bool try_add_operator_as_linear(OperatorId op, const std::vector<TensorId> &inputs, const std::vector<TensorId> &outputs, bool is_output = false) const
{
ARM_COMPUTE_UNUSED(op, is_output);
if(all_ops().empty())
{
return true;
}
// If the new operator is not the first operator, at least one input tensor must be
// the output tensor of the last non-output operator. All other input tensors must be
// the global input of the graph (i.e. not the output of any operator).
if(_last_op_available)
{
auto use_input_from_last_op = false;
for(auto src_tensor : inputs)
{
const auto src_ops = _adj_src_ops.find(src_tensor);
if(src_ops != _adj_src_ops.end())
{
ARM_COMPUTE_ERROR_ON(src_ops->second.size() > 1);
if(!src_ops->second.empty())
{
const auto src_op = src_ops->second[0];
if(src_op == _last_op)
{
if(use_input_from_last_op)
{
// To be safe, we also forbid using the output tensor
// of the last operator twice.
return false;
}
use_input_from_last_op = true;
}
else
{
// The input tensor of this operator must not be the output tensor
// of any other operator except the last non-output operator.
return false;
}
}
}
}
if(!use_input_from_last_op)
{
// At least one input tensor must be the output tensor of the last non-output operator.
return false;
}
}
// The output tensor of the new operator must not be the input tensor of any previously
// added operator.
for(auto dst_tensor : outputs)
{
if(_adj_dst_ops.find(dst_tensor) != _adj_dst_ops.end())
{
return false;
}
}
return true;
}
/** Add an operator, while keeping the graph as a "linear sequence"
*
* PRECONDITION: The current graph is already linear
* INVARIANT: The list can only grow from head to tail
* INVARIANT: POSTCONDITION: The graph is linear
*/
void add_operator_as_linear(OperatorId op, const std::vector<TensorId> &inputs, const std::vector<TensorId> &outputs, bool is_output = false)
{
const auto success = add_operator(op, inputs, outputs, is_output);
ARM_COMPUTE_UNUSED(success);
ARM_COMPUTE_ERROR_ON(!success);
}
/** Add a new operator
* Return invalid if it violates the DAG invariant
* Invalid operation will not change the graph
*
* @param[in] op Operator to add
* @param[in] inputs Input tensors to the operator
* @param[in] outputs Output tensors to the operator
* @param[in] is_output Whether this is an output operator
*/
bool add_operator(OperatorId op, const std::vector<TensorId> &inputs, const std::vector<TensorId> &outputs, bool is_output = false)
{
if(operator_exists(op))
{
return false;
}
_adj_src_tensors[op] = {};
_adj_dst_tensors[op] = {};
for(auto in_tensor : inputs)
{
// Linking input tensor to operator node will never create a cycle / loop because we guarantee
// each op is newly created, so every <input, op> pair / edge is new
link_input(op, in_tensor);
}
for(auto out_tensor : outputs)
{
// If there exists a back path from op's output tensor to op already, then linking the two will create a loop / cycle
if(path_exists_from_tensor_to_op(out_tensor, op))
{
remove_operator(op);
return false;
}
else
{
link_output(op, out_tensor);
}
}
if(!is_output)
{
_last_op_available = true;
_last_op = op;
}
return true;
}
/** Build a sequence of operators from the acyclic graph of operators.
*
* The graph will be visited in depth-first strategy. The operator can only be added to
* the sequence when all operators that supply the input tensors have been added. Otherwise,
* the operator will be ignored and later visited again. In other words, the dependency between
* operators will be preserved in the sequence.
*/
std::vector<OpPack> build_operators_sequence() const
{
std::vector<OpPack> ops_seq;
std::set<Id> done_ops;
std::set<Id> done_tensors;
const auto input_tensors = global_src_tensors();
for(auto tensor : input_tensors)
{
done_tensors.insert(tensor);
for(auto op : _adj_dst_ops.at(tensor))
{
build_operators_sequence_from_op(op, ops_seq, done_ops, done_tensors);
}
}
return ops_seq;
}
/** Strict equality comparison (all internal ids and order of insertion matter).
* In the future this may be replaced with a topological comparison, allowing equivalent graphs with different internal ids to be equal
*
*
* @param[in] g0
* @param[in] g1
* @return true If the same
* @return false Otherwise
*/
friend bool operator==(const DependencyGraph &g0, const DependencyGraph &g1)
{
// Do not compare id allocators
return std::make_tuple(
g0._adj_src_tensors, g0._adj_dst_tensors, g0._adj_src_ops, g0._adj_dst_ops)
== std::make_tuple(
g1._adj_src_tensors, g1._adj_dst_tensors, g1._adj_src_ops, g1._adj_dst_ops);
}
std::vector<OperatorId> src_ops_from_tensor(TensorId tensor) const
{
return _adj_src_ops.at(tensor);
}
std::vector<OperatorId> dst_ops_from_tensor(TensorId tensor) const
{
return _adj_dst_ops.at(tensor);
}
/** Get all tensors
*
* @return std::vector<TensorId>
*/
std::vector<TensorId> all_tensors() const
{
std::vector<TensorId> tensors{};
std::transform(std::begin(_adj_src_ops), std::end(_adj_src_ops), std::back_inserter(tensors), [](const auto & it)
{
return it.first;
});
return tensors;
}
/** Get source tensors of the whole graph
*
* @return std::vector<TensorId>
*/
std::vector<TensorId> global_src_tensors() const
{
std::vector<TensorId> tensors;
for(auto tensor_src_ops : _adj_src_ops)
{
if(tensor_src_ops.second.empty())
{
tensors.push_back(tensor_src_ops.first);
}
}
return tensors;
}
/** Get destination tensors of the whole graph
*
* @return std::vector<TensorId>
*/
std::vector<TensorId> global_dst_tensors() const
{
std::vector<TensorId> tensors;
for(auto tensor_dst_ops : _adj_dst_ops)
{
if(tensor_dst_ops.second.empty())
{
tensors.push_back(tensor_dst_ops.first);
}
}
return tensors;
}
/** Get intermediate tensors of the whole graph.
*
* @return std::vector<TensorId>
*/
std::vector<TensorId> intermediate_tensors() const
{
std::vector<TensorId> tensors;
// If a tensor is used to connect the input of an operator and the output of another operator,
// it is not allocated in the memory. The tensor exists as a temporary variable only.
for(auto src_tensor : _adj_src_ops)
{
if(!src_tensor.second.empty())
{
const auto dst_tensor = _adj_dst_ops.find(src_tensor.first);
if(dst_tensor != _adj_dst_ops.end())
{
if(!dst_tensor->second.empty())
{
tensors.push_back(src_tensor.first);
}
}
}
}
return tensors;
}
/** Get all root ops. Root ops can also be referred to as "src ops" of the whole graph
*
* @return std::vector<OperatorId>
*/
std::vector<OperatorId> get_root_ops() const
{
std::vector<OperatorId> ops{};
const auto op_list = all_ops();
for(auto op : op_list)
{
if(src_ops(op).empty())
{
ops.emplace_back(op);
}
}
return ops;
}
private:
void link_input(OperatorId op, TensorId in_tensor)
{
ARM_COMPUTE_ERROR_ON(!operator_exists(op));
if(!tensor_exists(in_tensor))
{
insert_new_tensor(in_tensor);
}
ARM_COMPUTE_ERROR_ON(are_connected(op, in_tensor)); // Prevent repetitive linking
_adj_src_tensors[op].push_back(in_tensor);
_adj_dst_ops[in_tensor].push_back(op);
}
void link_output(OperatorId op, TensorId out_tensor)
{
ARM_COMPUTE_ERROR_ON(!operator_exists(op));
if(!tensor_exists(out_tensor))
{
insert_new_tensor(out_tensor);
}
ARM_COMPUTE_ERROR_ON(are_connected(op, out_tensor)); // Prevent repetitive linking
_adj_dst_tensors[op].push_back(out_tensor);
_adj_src_ops[out_tensor].push_back(op);
}
std::vector<OperatorId> src_ops(OperatorId op) const
{
ARM_COMPUTE_ERROR_ON(!operator_exists(op));
std::vector<OperatorId> ops{};
for(TensorId src_tensor : src_tensors(op))
{
ops.insert(ops.end(), std::begin(_adj_src_ops.at(src_tensor)), std::end(_adj_src_ops.at(src_tensor)));
}
return ops;
}
std::vector<OperatorId> dst_ops(OperatorId op) const
{
ARM_COMPUTE_ERROR_ON(!operator_exists(op));
std::vector<OperatorId> ops{};
for(TensorId dst_tensor : _adj_dst_tensors.at(op))
{
ops.insert(ops.end(), std::begin(_adj_dst_ops.at(dst_tensor)), std::end(_adj_dst_ops.at(dst_tensor)));
}
return ops;
}
/** Get source tensors to an operator
*
* @param[in] op
* @return std::vector<TensorId>
*/
std::vector<TensorId> src_tensors(OperatorId op) const
{
ARM_COMPUTE_ERROR_ON(!operator_exists(op));
return _adj_src_tensors.at(op);
}
/** Get destination tensors to an operator
*
* @param[in] op
* @return std::vector<TensorId>
*/
std::vector<TensorId> dst_tensors(OperatorId op) const
{
ARM_COMPUTE_ERROR_ON(!operator_exists(op));
return _adj_dst_tensors.at(op);
}
/** Get all operators
*
* @return std::vector<OperatorId>
*/
std::vector<OperatorId> all_ops() const
{
std::vector<OperatorId> ops{};
std::transform(std::begin(_adj_src_tensors), std::end(_adj_src_tensors), std::back_inserter(ops), [](const auto & it)
{
return it.first;
});
return ops;
}
/** Remove an operator from graph.
*
* @param[in] op
*/
void remove_operator(OperatorId op)
{
for(auto src_tensor : _adj_src_tensors.at(op))
{
auto &dst_ops = _adj_dst_ops.at(src_tensor);
dst_ops.erase(
std::remove(std::begin(dst_ops), std::end(dst_ops), op),
std::end(dst_ops));
}
for(auto dst_tensor : _adj_dst_tensors.at(op))
{
auto &src_ops = _adj_src_ops.at(dst_tensor);
src_ops.erase(
std::remove(std::begin(src_ops), std::end(src_ops), op),
std::end(src_ops));
}
// Remove any isolated tensors
// An isolated tensor is one where both its _adj_src_ops and _adj_dst_ops are empty
for(auto t : all_tensors())
{
if(_adj_src_ops.at(t).empty() && _adj_dst_ops.at(t).empty())
{
_adj_src_ops.erase(t);
_adj_dst_ops.erase(t);
}
}
_adj_src_tensors.erase(op);
_adj_dst_tensors.erase(op);
}
void insert_new_tensor(TensorId tensor)
{
_adj_src_ops[tensor] = {};
_adj_dst_ops[tensor] = {};
}
bool tensor_exists(TensorId tensor) const
{
return _adj_src_ops.find(tensor) != _adj_src_ops.end() && _adj_dst_ops.find(tensor) != _adj_dst_ops.end();
}
bool operator_exists(OperatorId op) const
{
return _adj_src_tensors.find(op) != _adj_src_tensors.end() && _adj_dst_tensors.find(op) != _adj_dst_tensors.end();
}
bool is_src_tensor_of(OperatorId op, TensorId tensor) const
{
if(!operator_exists(op) || !tensor_exists(tensor))
{
return false;
}
const auto op_inputs = src_tensors(op);
return std::find(op_inputs.begin(), op_inputs.end(), tensor) != op_inputs.end();
}
bool is_dst_tensor_of(OperatorId op, TensorId tensor) const
{
if(!operator_exists(op) || !tensor_exists(tensor))
{
return false;
}
const auto op_outputs = dst_tensors(op);
return std::find(op_outputs.begin(), op_outputs.end(), tensor) != op_outputs.end();
}
bool are_connected(OperatorId op, TensorId tensor) const
{
return is_src_tensor_of(op, tensor) || is_dst_tensor_of(op, tensor);
}
/** If op is the destination / leaf operator of the whole graph
*
* @param[in] op
* @return true
* @return false
*/
bool is_dst_op(OperatorId op) const
{
return dst_ops(op).empty();
}
std::vector<OperatorId> get_dst_ops() const
{
std::vector<OperatorId> ops{};
const auto op_list = all_ops();
for(auto op : op_list)
{
if(is_dst_op(op))
{
ops.emplace_back(op);
}
}
return ops;
}
bool path_exists_from_tensor_to_op(TensorId src_tensor, OperatorId dst_op) const
{
if(!tensor_exists(src_tensor) || !operator_exists(dst_op))
{
return false;
}
for(auto child_op : dst_ops_from_tensor(src_tensor))
{
if(path_exists_from_op_to_op(child_op, dst_op))
{
return true;
}
}
return false;
}
bool path_exists_from_op_to_op(OperatorId src_op, OperatorId dst_op) const
{
if(!operator_exists(src_op) || !operator_exists(dst_op))
{
return false;
}
if(src_op == dst_op)
{
return true;
}
if(is_in(src_op, get_dst_ops()))
{
return false;
}
for(auto child_tensor : dst_tensors(src_op))
{
if(path_exists_from_tensor_to_op(child_tensor, dst_op))
{
return true;
}
}
return false;
}
void build_operators_sequence_from_op(
Id op,
std::vector<OpPack> &ops_seq,
std::set<Id> &done_ops,
std::set<Id> &done_tensors) const
{
while(true)
{
// If the operator has been added to the sequence, ignore it.
if(done_ops.find(op) != done_ops.end())
{
return;
}
// If not all the input tensors of the operator are available, this operator cannot be
// added to the sequence for now. It will be visited again after the source operator
// is added to the sequence.
const auto src_tensors = _adj_src_tensors.at(op);
for(auto src : src_tensors)
{
if(done_tensors.find(src) == done_tensors.end())
{
return;
}
}
// This operator is ready to be added to the sequence.
const auto dst_tensors = _adj_dst_tensors.at(op);
done_ops.insert(op);
OpPack pack{ op, src_tensors, dst_tensors };
ops_seq.push_back(pack);
done_tensors.insert(dst_tensors.begin(), dst_tensors.end());
// Visit all the sink operators.
// Call this function recursively unless there is only one sink.
if(dst_tensors.size() == 1 && _adj_dst_ops.at(dst_tensors[0]).size() == 1)
{
op = _adj_dst_ops.at(dst_tensors[0])[0];
}
else
{
for(auto dst_tensor : dst_tensors)
{
const auto dst_ops = _adj_dst_ops.at(dst_tensor);
for(auto dst_op : dst_ops)
{
build_operators_sequence_from_op(dst_op, ops_seq, done_ops, done_tensors);
}
}
return;
}
}
}
private:
AdjList _adj_src_tensors{};
AdjList _adj_dst_tensors{};
AdjList _adj_src_ops{};
AdjList _adj_dst_ops{};
bool _last_op_available{ false };
OperatorId _last_op{ 0 };
};
} // namespace dynamic_fusion
} // namespace experimental
} // namespace arm_compute
#endif /* SRC_DYNAMIC_FUSION_SKETCH_UTILS_DEPENDENCYGRAPH */