| /* |
| * 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, ¤t_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 |