include/WeakPartialOrdering.h (419 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /* * Implementation of a weak partial ordering (WPO) of a rooted directed graph, * as described in the paper: * * Sung Kook Kim, Arnaud J. Venet, and Aditya V. Thakur. * Deterministic Parallel Fixpoint Computation. * * https://dl.acm.org/ft_gateway.cfm?id=3371082 * */ #pragma once #include <boost/pending/disjoint_sets.hpp> #include <cstddef> #include <functional> #include <future> #include <memory> #include <queue> #include <set> #include <stack> #include <tuple> #include <unordered_map> #include <unordered_set> #include <utility> #include <vector> #include "Exceptions.h" namespace sparta { // Forward declarations template <typename NodeId> class WpoNode; template <typename NodeId, typename NodeHash> class WeakPartialOrdering; namespace wpo_impl { template <typename NodeId, typename NodeHash> class WpoBuilder; } // end namespace wpo_impl /* * A node of a weak partial ordering. * It is either head, plain, or exit. */ template <typename NodeId> class WpoNode final { public: enum class Type { Plain, Head, Exit }; private: // Uses index of an array to id Wpo nodes. using WpoIdx = uint32_t; private: NodeId m_node; Type m_type; // Size of maximal SCC with this as its header. uint32_t m_size; // Successors of scheduling constraints. std::set<WpoIdx> m_successors; // Predecessors of scheduling constraints. std::set<WpoIdx> m_predecessors; // Number of outer predecessors w.r.t. the component (for exits only). std::unordered_map<WpoIdx, uint32_t> m_num_outer_preds; public: WpoNode(const NodeId& node, Type type, uint32_t size) : m_node(node), m_type(type), m_size(size) {} public: // Return the NodeId for this node const NodeId& get_node() const { return m_node; } // Check the type of this node bool is_plain() const { return m_type == Type::Plain; } bool is_head() const { return m_type == Type::Head; } bool is_exit() const { return m_type == Type::Exit; } // Get successors. const std::set<WpoIdx>& get_successors() const { return m_successors; } // Get predecessors. const std::set<WpoIdx>& get_predecessors() const { return m_predecessors; } // Get number of predecessors. uint32_t get_num_preds() const { return m_predecessors.size(); } // Get number of outer predecessors w.r.t. the component (for exits only). const std::unordered_map<WpoIdx, uint32_t>& get_num_outer_preds() const { RUNTIME_CHECK(m_type == Type::Exit, undefined_operation()); return m_num_outer_preds; } // Get size of the SCC. uint32_t get_size() const { return m_size; } private: // Add successor. void add_successor(WpoIdx idx) { m_successors.insert(idx); } // Add predecessor. void add_predecessor(WpoIdx idx) { m_predecessors.insert(idx); } // Check if the given node is a successor. bool is_successor(WpoIdx idx) { return m_successors.find(idx) != m_successors.end(); } // Increment the number of outer predecessors. void inc_num_outer_preds(WpoIdx idx) { RUNTIME_CHECK(m_type == Type::Exit, internal_error()); m_num_outer_preds[idx]++; } public: WpoNode(WpoNode&&) = default; WpoNode(const WpoNode&) = delete; WpoNode& operator=(const WpoNode&) = delete; WpoNode& operator=(WpoNode&&) = delete; template <typename T1, typename T2> friend class wpo_impl::WpoBuilder; }; // end class WpoNode /* * Implementation of the decomposition of a rooted directed graph into a weak * partial ordering (WPO). A technical paper to appear in POPL 2020: * * Sung Kook Kim, Arnaud J. Venet, and Aditya V. Thakur. * Deterministic Parallel Fixpoint Computation. * * https://dl.acm.org/ft_gateway.cfm?id=3371082 * */ template <typename NodeId, typename NodeHash = std::hash<NodeId>> class WeakPartialOrdering final { private: using WpoNodeT = WpoNode<NodeId>; using Type = typename WpoNodeT::Type; using WpoIdx = uint32_t; private: // WPO nodes. std::vector<WpoNodeT> m_nodes; // Top level nodes. Nodes that are outside of any component. // This is NOT used in the concurrent fixpoint iteration. std::vector<WpoIdx> m_toplevel; // post DFN of the nodes. std::unordered_map<NodeId, uint32_t, NodeHash> m_post_dfn; // This is used when generating a WTO from a WPO. // See algorithm ConstructWTO^{BU} in Section 7 of the POPL 2020 paper. bool m_lifted; // Get the post DFN of a node. uint32_t get_post_dfn(NodeId n) const { auto it = m_post_dfn.find(n); if (it != m_post_dfn.end()) { return it->second; } return 0; } public: // Construct a WPO for the given CFG. WeakPartialOrdering( const NodeId& root, std::function<std::vector<NodeId>(const NodeId&)> successors, bool lift) : m_lifted(lift) { if (successors(root).empty()) { m_nodes.emplace_back(root, Type::Plain, /*size=*/1); m_toplevel.push_back(0); m_post_dfn[root] = 1; return; } wpo_impl::WpoBuilder<NodeId, NodeHash> builder( successors, m_nodes, m_toplevel, m_post_dfn, lift); builder.build(root); } // Total number of nodes in this wpo. uint32_t size() const { return m_nodes.size(); } // Entry node of this wpo. WpoIdx get_entry() { return m_nodes.size() - 1; } // Successors of the node. const std::set<WpoIdx>& get_successors(WpoIdx idx) const { return m_nodes[idx].get_successors(); } // Predecessors of the node. const std::set<WpoIdx>& get_predecessors(WpoIdx idx) const { return m_nodes[idx].get_predecessors(); } // Number of predecessors of the node. uint32_t get_num_preds(WpoIdx idx) const { return m_nodes[idx].get_num_preds(); } // Get number of outer preds for the exit's component. const std::unordered_map<WpoIdx, uint32_t>& get_num_outer_preds( WpoIdx exit) const { return m_nodes[exit].get_num_outer_preds(); } // Head of the exit node. WpoIdx get_head_of_exit(WpoIdx exit) const { return exit + 1; } // Exit of the head node. WpoIdx get_exit_of_head(WpoIdx head) const { return head - 1; } // NodeId for the node. const NodeId& get_node(WpoIdx idx) const { return m_nodes[idx].get_node(); } // Type queries for node. bool is_plain(WpoIdx idx) const { return m_nodes[idx].is_plain(); } bool is_head(WpoIdx idx) const { return m_nodes[idx].is_head(); } bool is_exit(WpoIdx idx) const { return m_nodes[idx].is_exit(); } // Check whether a predecessor is outside of the component of the head. // This can be used to detect whether an edge is a back edge: // is_backedge(head, pred) // := !is_from_outside(head, pred) /\ is_predecessor(head, pred) // This is used in interleaved widening and narrowing. bool is_from_outside(NodeId head, NodeId pred) { return get_post_dfn(head) < get_post_dfn(pred); } WeakPartialOrdering(const WeakPartialOrdering& other) = delete; WeakPartialOrdering(WeakPartialOrdering&& other) = delete; WeakPartialOrdering& operator=(const WeakPartialOrdering& other) = delete; WeakPartialOrdering& operator=(WeakPartialOrdering&& other) = delete; }; // end class WeakPartialOrdering namespace wpo_impl { template <typename NodeId, typename NodeHash> class WpoBuilder final { private: using WpoNodeT = WpoNode<NodeId>; using WpoT = WeakPartialOrdering<NodeId, NodeHash>; using Type = typename WpoNodeT::Type; using WpoIdx = uint32_t; public: WpoBuilder(std::function<std::vector<NodeId>(const NodeId&)> successors, std::vector<WpoNodeT>& wpo_space, std::vector<WpoIdx>& toplevel, std::unordered_map<NodeId, uint32_t, NodeHash>& post_dfn, bool lift) : m_successors(successors), m_wpo_space(wpo_space), m_toplevel(toplevel), m_post_dfn(post_dfn), m_next_dfn(1), m_next_post_dfn(1), m_next_idx(0), m_lift(lift) {} void build(const NodeId& root) { construct_auxilary(root); construct_wpo(); // Compute num_outer_preds. for (auto& p : m_for_outer_preds) { auto& v = p.first; auto& x_max = p.second; auto h = m_wpo_space[v].is_head() ? v : m_parent[v]; // index of exit == index of head - 1. auto x = h - 1; while (x != x_max) { m_wpo_space[x].inc_num_outer_preds(v); h = m_parent[h]; x = h - 1; } m_wpo_space[x].inc_num_outer_preds(v); } } private: // Construct auxilary data-structures. // Performs DFS iteratively to classify the edges and find lowest common // ancestors of cross/forward edges. // Nodes are identified by their DFNs in the builder. void construct_auxilary(const NodeId& root) { // It uses disjoint-sets data structure. typedef std::unordered_map<uint32_t, std::size_t> rank_t; typedef std::unordered_map<uint32_t, uint32_t> parent_t; rank_t rank_map; parent_t parent_map; typedef boost::associative_property_map<rank_t> r_pmap_t; r_pmap_t r_pmap(rank_map); typedef boost::associative_property_map<parent_t> p_pmap_t; p_pmap_t p_pmap(parent_map); boost::disjoint_sets<r_pmap_t, p_pmap_t> dsets(r_pmap, p_pmap); std::unordered_map<uint32_t, uint32_t> ancestor; std::stack<std::tuple<NodeId, bool, uint32_t>> stack; std::unordered_map<uint32_t, bool> black; stack.push(std::make_tuple(root, false, 0)); while (!stack.empty()) { // Iterative DFS. auto& stack_top = stack.top(); auto vertex_ref = std::get<0>(stack_top); auto finished = std::get<1>(stack_top); auto pred = std::get<2>(stack_top); stack.pop(); if (finished) { // DFS is done with this vertex. // Set the post DFN. m_post_dfn[vertex_ref] = m_next_post_dfn++; auto vertex = get_dfn(vertex_ref); // Mark visited. black[vertex] = true; dsets.union_set(vertex, pred); ancestor[dsets.find_set(pred)] = pred; } else { if (get_dfn(vertex_ref) != 0 /* means that the vertex is already discovered. */) { // A forward edge. // Forward edges can be ignored, as they are redundant. continue; } // New vertex is discovered. auto vertex = m_next_dfn++; push_ref(vertex_ref); set_dfn(vertex_ref, vertex); dsets.make_set(vertex); ancestor[vertex] = vertex; // This will be popped after all its successors are finished. stack.push(std::make_tuple(vertex_ref, true, pred)); auto successors = m_successors(vertex_ref); // Successors vector is reversed to match the order with WTO. for (auto rit = successors.rbegin(); rit != successors.rend(); ++rit) { auto succ = get_dfn(*rit); if (succ == 0 /* 0 means that vertex is undiscovered. */) { // Newly discovered vertex. Search continues. stack.push(std::make_tuple(*rit, false, vertex)); } else if (black[succ]) { // A cross edge. auto lca = ancestor[dsets.find_set(succ)]; m_cross_fwds[lca].emplace_back(vertex, succ); } else { // A back edge. m_back_preds[succ].push_back(vertex); } } if (pred != 0) { // A tree edge. m_non_back_preds[vertex].push_back(pred); } } } } void construct_wpo() { std::vector<uint32_t> rank(get_next_dfn()); std::vector<uint32_t> parent(get_next_dfn()); // A partition of vertices. Each subset is known to be strongly connected. boost::disjoint_sets<uint32_t*, uint32_t*> dsets(&rank[0], &parent[0]); // Maps representative of a set to the vertex with minimum DFN. std::vector<uint32_t> rep(get_next_dfn()); // Maps a head to its exit. std::vector<uint32_t> exit(get_next_dfn()); // Maps a vertex to original non-back edges that now target the vertex. std::vector<std::vector<std::pair<uint32_t, uint32_t>>> origin( get_next_dfn()); // Maps a head to its size of components. std::vector<uint32_t> size(get_next_dfn()); // Index of WpoNode in wpo space. m_d2i.resize(2 * get_next_dfn()); // DFN that will be assigned to the next exit. uint32_t dfn = get_next_dfn(); // Initialization. for (uint32_t v = 1; v < get_next_dfn(); v++) { dsets.make_set(v); rep[v] = exit[v] = v; const auto& non_back_preds_v = m_non_back_preds[v]; auto& origin_v = origin[v]; origin_v.reserve(origin_v.size() + non_back_preds_v.size()); for (auto u : non_back_preds_v) { origin_v.emplace_back(u, v); } } // In reverse DFS order, build WPOs for SCCs bottom-up. for (uint32_t h = get_next_dfn() - 1; h > 0; h--) { // Restore cross/fwd edges which has h as the LCA. auto it = m_cross_fwds.find(h); if (it != m_cross_fwds.end()) { for (auto& edge : it->second) { // edge: u -> v auto& u = edge.first; auto& v = edge.second; auto rep_v = rep[dsets.find_set(v)]; m_non_back_preds[rep_v].push_back(u); origin[rep_v].emplace_back(u, v); } } // Find nested SCCs. bool is_SCC = false; std::unordered_set<uint32_t> backpreds_h; for (auto v : m_back_preds[h]) { if (v != h) { backpreds_h.insert(rep[dsets.find_set(v)]); } else { // Self-loop. is_SCC = true; } } if (!backpreds_h.empty()) { is_SCC = true; } // Invariant: h \notin backpreds_h. std::unordered_set<uint32_t> nested_SCCs_h(backpreds_h); std::vector<uint32_t> worklist_h(backpreds_h.begin(), backpreds_h.end()); while (!worklist_h.empty()) { auto v = worklist_h.back(); worklist_h.pop_back(); for (auto p : m_non_back_preds[v]) { auto rep_p = rep[dsets.find_set(p)]; auto p_it = nested_SCCs_h.find(rep_p); if (p_it == nested_SCCs_h.end() && rep_p != h) { nested_SCCs_h.insert(rep_p); worklist_h.push_back(rep_p); } } } // Invariant: h \notin nested_SCCs_h. // h is a Trivial SCC. if (!is_SCC) { size[h] = 1; add_node(h, get_ref(h), Type::Plain, /*size=*/1); // Invariant: wpo_space = ...::h continue; } // Compute the size of the component C_h. // Size of this component is initialized to 2: the head and the exit. uint32_t size_h = 2; for (auto v : nested_SCCs_h) { size_h += size[v]; } size[h] = size_h; // Invariant: size_h = size[h] = number of nodes in the component C_h. // Add new exit x_h. auto x_h = dfn++; add_node(x_h, get_ref(h), Type::Exit, size_h); add_node(h, get_ref(h), Type::Head, size_h); // Invariant: wpo_space = ...::x_h::h if (backpreds_h.empty()) { // Add scheduling constraints from h to x_h add_successor(/*from=*/h, /*to=*/x_h, /*exit=*/x_h, /*outer_pred?=*/false); } else { for (auto p : backpreds_h) { add_successor(/*from=*/exit[p], /*to=*/x_h, /*exit=*/x_h, /*outer_pred?=*/false); } } // Invariant: Scheduling contraints to x_h are all constructed. // Add scheduling constraints between the WPOs for nested SCCs. for (auto v : nested_SCCs_h) { for (auto& edge : origin[v]) { auto& u = edge.first; auto& vv = edge.second; auto& x_u = exit[rep[dsets.find_set(u)]]; auto& x_v = exit[v]; // Invariant: u -> vv, u \notin C_v, vv \in C_v, u \in C_h, v \in C_h. if (m_lift) { add_successor(/*from=*/x_u, /*to=*/v, /*exit=*/x_v, /*outer_pred?=*/x_v != v); // Invariant: x_u \in get_predecessors(v). } else { add_successor(/*from=*/x_u, /*to=*/vv, /*exit=*/x_v, /*outer_pred?=*/x_v != v); // Invariant: x_u \in get_predecessors(vv). } } } // Invariant: WPO for SCC with h as its header is constructed. // Update the partition by merging. for (auto v : nested_SCCs_h) { dsets.union_set(v, h); rep[dsets.find_set(v)] = h; m_parent[index_of(v)] = index_of(h); } // Set exit of h to x_h. exit[h] = x_h; // Invariant: exit[h] = h if C_h is trivial SCC, x_h otherwise. } // Add scheduling constraints between the WPOs for maximal SCCs. m_toplevel.reserve(get_next_dfn()); for (uint32_t v = 1; v < get_next_dfn(); v++) { if (rep[dsets.find_set(v)] == v) { add_toplevel(v); m_parent[index_of(v)] = index_of(v); for (auto& edge : origin[v]) { auto& u = edge.first; auto& vv = edge.second; auto& x_u = exit[rep[dsets.find_set(u)]]; auto& x_v = exit[v]; // Invariant: u -> vv, u \notin C_v, vv \in C_v, u \in C_h, v \in C_h. if (m_lift) { add_successor(/*from=*/x_u, /*to=*/v, /*exit=*/x_v, /*outer_pred?=*/x_v != v); // Invariant: x_u \in get_predecessors(v). } else { add_successor(/*from=*/x_u, /*to=*/vv, /*exit=*/x_v, /*outer_pred?=*/x_v != v); // Invariant: x_u \in get_predecessors(vv). } } } } // Invariant: WPO for the CFG is constructed. } uint32_t get_dfn(NodeId n) { auto it = m_dfn.find(n); if (it != m_dfn.end()) { return it->second; } return 0; } void set_dfn(NodeId n, uint32_t num) { m_dfn[n] = num; } const NodeId& get_ref(uint32_t num) const { return m_ref.at(num - 1); } void push_ref(NodeId n) { m_ref.push_back(n); } uint32_t get_next_dfn() const { // Includes exits. // 0 represents invalid node. return m_next_dfn; } void add_node(uint32_t dfn, NodeId ref, Type type, uint32_t size) { m_d2i[dfn] = m_next_idx++; m_wpo_space.emplace_back(ref, type, size); } WpoNodeT& node_of(uint32_t dfn) { return m_wpo_space[index_of(dfn)]; } WpoIdx index_of(uint32_t dfn) { return m_d2i[dfn]; } void add_successor(uint32_t from, uint32_t to, uint32_t exit, bool outer_pred) { auto fromIdx = index_of(from); auto toIdx = index_of(to); auto& fromNode = node_of(from); auto& toNode = node_of(to); if (!fromNode.is_successor(toIdx)) { if (outer_pred) { m_for_outer_preds.push_back(std::make_pair(toIdx, index_of(exit))); } fromNode.add_successor(toIdx); toNode.add_predecessor(fromIdx); } } void add_toplevel(uint32_t what) { m_toplevel.push_back(index_of(what)); } std::function<std::vector<NodeId>(const NodeId&)> m_successors; // A reference to Wpo space (array of Wpo nodes). std::vector<WpoNodeT>& m_wpo_space; // A reference to Wpo space that contains only the top level nodes. std::vector<WpoIdx>& m_toplevel; // A map from NodeId to DFN. std::unordered_map<NodeId, uint32_t, NodeHash> m_dfn; // A map from NodeId to post DFN. std::unordered_map<NodeId, uint32_t, NodeHash>& m_post_dfn; // A map from DFN to NodeId. std::vector<NodeId> m_ref; // A map from DFN to DFNs of its backedge predecessors. std::unordered_map<uint32_t, std::vector<uint32_t>> m_back_preds; // A map from DFN to DFNs of its non-backedge predecessors. std::unordered_map<uint32_t, std::vector<uint32_t>> m_non_back_preds; // A map from DFN to cross/forward edges (DFN is the lowest common ancestor). std::unordered_map<uint32_t, std::vector<std::pair<uint32_t, uint32_t>>> m_cross_fwds; // Increase m_num_outer_preds[x][pair.first] for component C_x that satisfies // pair.first \in C_x \subseteq C_{pair.second}. std::vector<std::pair<WpoIdx, WpoIdx>> m_for_outer_preds; // A map from node to the head of minimal component that contains it as // non-header. std::unordered_map<WpoIdx, WpoIdx> m_parent; // Maps DFN to WpoIdx. std::vector<uint32_t> m_d2i; // Next DFN to assign. uint32_t m_next_dfn; // Next post DFN to assign. uint32_t m_next_post_dfn; // Next WpoIdx to assign. uint32_t m_next_idx; // 'Lift' the scheduling constraints when adding them. bool m_lift; }; // end class wpo_builder } // end namespace wpo_impl } // end namespace sparta