blob: 2828a2f69d03d7594312ff3e0b1cc34dd58d8d24 [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 detect_recursion.cc
*
* \brief Analysis to detect global recursive or mutually recursive functions.
*/
#include <tvm/ffi/cast.h>
#include <tvm/ffi/reflection/registry.h>
#include <tvm/relax/analysis.h>
#include <tvm/relax/expr_functor.h>
#include <tvm/tirx/expr_functor.h>
namespace tvm {
namespace relax {
/*
* General approach to detecting recursion:
* Suppose we have a dependency graph of global functions,
* where function A depends on function B if A contains a reference to B
* (i.e., an edge A->B means A references B). If function A is recursive,
* then it has a self-edge A->A.
*
* Note that the call can happen _anywhere_ in the function's body:
* All that is important for mutual recursion is that one function
* needs the other to be in scope (it needs to know about it) to define
* the body. This includes calls that happen inside local function definitions,
* branches that may not execute, etc.
*
* Then detecting simple recursion and mutual recursion is a problem of cycle
* detection: Functions F1, F2, ..., Fn are mutually recursive if there exists
* a single directed cycle that contains all of them.
*
* We aim to find the _largest_ directed cycles in the graph, as there can
* be smaller cycles within the larger ones, as in the following example:
*
* A <-> B <-> C
* ^ | ^
* | v |
* | D |
* | | |
* v v v
* E <-> F <-> G
*
* Handling a case like this in a directed graph is very difficult
* because most simple algorithms (variations of DFS) aim to find the smallest
* cycle, but in this case, we have multiple cycles that go through nodes multiple times:
* E.g., A->B->D->F->E->A, B->C->G->F->D->B, and A->B->C->G->F->E->A.
* However, we would consider _all_ of these nodes to be mutually recursive,
* and there is a single cycle: A->B->C->G->F->E->A->B->D->F->E->A (must go through A twice)
*
* We can use Johnson's elementary circuit-finding algorithm (1975):
* https://epubs.siam.org/doi/10.1137/0204007
* and find all elementary circuits in the graph, which are cycles that go
* through nodes at most once.
*
* With all the elementary cycles, we can coalesce different cycles that involve the
* same node, which would all form a group of mutually recursive functions
*/
class DependencyGatherer : public ExprVisitor {
public:
explicit DependencyGatherer(const IRModule& m) : m_(m) {}
std::unordered_set<std::string> Track(const Function& func) {
this->VisitExpr(func);
return deps_;
}
void VisitExpr_(const GlobalVarNode* gv) override {
// disregard PrimFuncs
if (!m_->Lookup(ffi::GetRef<GlobalVar>(gv)).as<relax::FunctionNode>()) {
return;
}
deps_.insert(gv->name_hint);
}
private:
std::unordered_set<std::string> deps_;
const IRModule& m_;
};
using adjacency_map = std::unordered_map<std::string, std::unordered_set<std::string>>;
using node_set = std::unordered_set<size_t>;
using adjacency_index = std::vector<node_set>;
adjacency_map GatherDependencyGraph(const IRModule& m) {
adjacency_map ret;
for (auto gv_func : m->functions) {
const relax::FunctionNode* func = gv_func.second.as<relax::FunctionNode>();
// disregard PrimFuncs and the like
if (!func) {
continue;
}
std::string name = gv_func.first->name_hint;
auto deps = DependencyGatherer(m).Track(ffi::GetRef<relax::Function>(func));
ret.insert({name, deps});
}
return ret;
}
// the graph algorithm pseudocode assumes vertices are indices and makes use of the fact you can
// increment them, so for ease, we convert the strings to indices by some ordering
adjacency_index ConvertToIndices(const adjacency_map& graph,
const std::vector<std::string>& ordering) {
adjacency_index ret;
for (size_t i = 0; i < ordering.size(); i++) {
std::string current = ordering[i];
node_set neighbors;
for (size_t j = 0; j < ordering.size(); j++) {
if (graph.at(current).count(ordering[j])) {
neighbors.insert(j);
}
}
ret.push_back(neighbors);
}
return ret;
}
/********* Strongly connected component (SCC) detection, needed for Johnson's algorithm *********/
// Based on the pseudocode for Tarjan's SCC detection algorithm
// See: https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
// Modification: We take a min_vert parameter to ignore all vertices below that.
// This is because Johnson's algorithm searches for SCCs on a subgraph of
// all vertices after a certain one (per some arbitrary ordering)
void StronglyConnect(size_t node, const adjacency_index& graph, size_t min_vert,
// use signed ints so that -1 can indicate undefined/unvisited
std::vector<int>* indices, std::vector<int>* low_links,
std::vector<size_t>* stack, std::vector<bool>* on_stack,
std::vector<node_set>* sccs, int* running_index) {
indices->operator[](node) = *running_index;
low_links->operator[](node) = *running_index;
(*running_index)++;
stack->push_back(node);
on_stack->operator[](node) = true;
auto children = graph.at(node);
for (auto child : children) {
// ignore children outside the verts we are checking
if (child < min_vert) {
continue;
}
if (indices->at(child) == -1) {
StronglyConnect(child, graph, min_vert, indices, low_links, stack, on_stack, sccs,
running_index);
low_links->operator[](node) = std::min(low_links->at(node), low_links->at(child));
} else if (on_stack->at(child)) {
low_links->operator[](node) = std::min(low_links->at(node), indices->at(child));
}
}
// root node -> have found an SCC
if (low_links->at(node) == indices->at(node)) {
node_set scc;
size_t m;
do {
m = stack->back();
stack->pop_back();
on_stack->operator[](m) = false;
scc.insert(m);
} while (m != node);
sccs->push_back(scc);
}
}
std::vector<node_set> FindStronglyConnectedComponents(const adjacency_index& graph,
size_t min_vert) {
std::vector<size_t> stack;
std::vector<node_set> sccs;
int running_index = 0;
std::vector<int> indices;
std::vector<int> low_links;
std::vector<bool> on_stack;
for (size_t i = 0; i < graph.size(); i++) {
indices.push_back(-1);
low_links.push_back(-1);
on_stack.push_back(false);
}
for (size_t i = min_vert; i < graph.size(); i++) {
StronglyConnect(i, graph, min_vert, &indices, &low_links, &stack, &on_stack, &sccs,
&running_index);
}
return sccs;
}
/********* Helper functions needed for Johnson's algorithm *********/
// return strongly connected componenet containing the least vertex
node_set GetLeastSCC(const std::vector<node_set>& sccs) {
int min_idx = 0;
bool min_found = false;
size_t min = 0;
for (size_t i = 0; i < sccs.size(); i++) {
if (!min_found) {
min = *(sccs[i].begin());
min_found = true;
min_idx = i;
}
for (size_t v : sccs[i]) {
if (v < min) {
min = v;
min_idx = i;
}
}
}
return sccs[min_idx];
}
size_t LeastVertex(const node_set& scc) {
bool min_found = false;
size_t min = 0;
for (size_t v : scc) {
if (!min_found) {
min = v;
min_found = true;
}
if (v < min) {
min = v;
}
}
return min;
}
/********* Johnson's algorithm implementation *********/
// implementation is based directly on the pseudocode from
// "Finding All the Elementary Circuits of a Directed Graph" (Johnson, 1975)
void Unblock(std::vector<bool>* blocked, std::vector<node_set>* blocked_nodes, size_t node) {
blocked->operator[](node) = false;
// copy so we don't modify the set we're iterating on
auto blocked_on_node = node_set(blocked_nodes->at(node));
for (auto blocked_node : blocked_on_node) {
blocked_nodes->at(node).erase(blocked_node);
if (blocked->at(blocked_node)) {
Unblock(blocked, blocked_nodes, blocked_node);
}
}
}
bool CheckCircuit(const adjacency_index& graph, const node_set& scc,
std::vector<node_set>* blocked_nodes, std::vector<bool>* blocked,
std::vector<size_t>* current_stack, std::vector<node_set>* found_circuits,
size_t s, size_t v) {
bool f = false;
blocked->operator[](v) = true;
current_stack->push_back(v);
for (size_t child : graph[v]) {
// ignore any node that's not in the SCC:
// the algorithm considers only the subgraph pertaining to the SCC
if (!scc.count(child)) {
continue;
}
if (child == s) {
// we found a circuit, so report it
auto new_circuit = node_set(current_stack->begin(), current_stack->end());
new_circuit.insert(s);
found_circuits->push_back(new_circuit);
f = true;
} else if (!blocked->at(child)) {
if (CheckCircuit(graph, scc, blocked_nodes, blocked, current_stack, found_circuits, s,
child)) {
f = true;
}
}
}
if (f) {
Unblock(blocked, blocked_nodes, v);
} else {
for (size_t child : graph[v]) {
if (!scc.count(child)) {
continue;
}
if (!blocked_nodes->at(child).count(v)) {
blocked_nodes->at(child).insert(v);
}
}
}
current_stack->pop_back();
return f;
}
std::vector<node_set> DetectElementaryCircuits(const adjacency_index& graph) {
std::vector<node_set> blocked_nodes;
for (size_t i = 0; i < graph.size(); i++) {
blocked_nodes.push_back(node_set());
}
std::vector<bool> blocked;
for (size_t i = 0; i < graph.size(); i++) {
blocked.push_back(false);
}
std::vector<size_t> current_stack;
std::vector<node_set> found_circuits;
size_t s = 0;
while (s < graph.size()) {
auto sccs = FindStronglyConnectedComponents(graph, s);
auto scc = GetLeastSCC(sccs);
s = LeastVertex(scc);
// Note: the pseudocode calls for an early exit if the subgraph is empty.
// However, that will never happen (there will always be at least one SCC
// with at least one node)
for (size_t i = s; i < graph.size(); i++) {
if (!scc.count(i)) {
continue;
}
blocked[i] = false;
blocked_nodes[i].clear();
}
CheckCircuit(graph, scc, &blocked_nodes, &blocked, &current_stack, &found_circuits, s, s);
s++;
}
return found_circuits;
}
/********* Coalescing the circuits and returning the results *********/
// given all elementary circuits, we want to coalesce any circuits that share nodes
std::vector<node_set> CoalesceCircuits(const std::vector<node_set>& circuits) {
std::vector<node_set> ret;
std::unordered_set<size_t> merged;
bool changed = false;
for (size_t i = 0; i < circuits.size(); i++) {
if (merged.count(i)) {
continue;
}
node_set current(circuits[i].begin(), circuits[i].end());
for (size_t j = i + 1; j < circuits.size(); j++) {
if (merged.count(j)) {
continue;
}
for (size_t member : current) {
if (circuits[j].count(member)) {
changed = true;
merged.insert(j);
current.insert(circuits[j].begin(), circuits[j].end());
}
}
}
ret.push_back(current);
}
// try again if something changed, as there may be more chances to coalesce
if (changed) {
return CoalesceCircuits(ret);
}
return ret;
}
tvm::ffi::Array<tvm::ffi::Array<GlobalVar>> DetectRecursion(const IRModule& m) {
auto graph = GatherDependencyGraph(m);
// have to decide on some ordering for names
std::vector<std::string> name_ordering;
for (auto kv : graph) {
name_ordering.push_back(kv.first);
}
auto indices = ConvertToIndices(graph, name_ordering);
auto groups = CoalesceCircuits(DetectElementaryCircuits(indices));
// convert to expected representation
tvm::ffi::Array<tvm::ffi::Array<GlobalVar>> ret;
for (auto group : groups) {
tvm::ffi::Array<GlobalVar> found;
for (size_t node : group) {
found.push_back(m->GetGlobalVar(name_ordering[node]));
}
ret.push_back(found);
}
return ret;
}
TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::GlobalDef().def("relax.analysis.detect_recursion", DetectRecursion);
}
} // namespace relax
} // namespace tvm