// -*- mode: C++; c-file-style: "cc-mode" -*- //************************************************************************* // DESCRIPTION: Verilator: Multi-threaded MTask graph contraction (coarsening) // // Code available from: https://verilator.org // //************************************************************************* // // This program is free software; you can redistribute it and/or modify it // under the terms of either the GNU Lesser General Public License Version 3 // or the Perl Artistic License Version 2.0. // SPDX-FileCopyrightText: 2003-2026 Wilson Snyder // SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 // //************************************************************************* // // Coarsens the fine-grained MTask graph produced by the partitioner by // repeatedly contracting MTasks (merging along an edge, or merging two // "sibling" MTasks) until a critical-path score limit is reached. Driven by // the partitioner in V3OrderParallel.cpp via OrderMTaskGraph::contract, // declared in V3OrderMTaskGraph.h. // //************************************************************************* #include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT #include "V3Global.h" #include "V3Graph.h" #include "V3GraphStream.h" #include "V3OrderMTaskGraph.h" #include "V3PairingHeap.h" #include #include #include #include #include #include #include #include VL_DEFINE_DEBUG_FUNCTIONS; class MergeCandidate; class SiblingMC; class EdgeMC; // ###################################################################### // Partitioner tunable settings: // // Before describing these settings, a bit of background: // // Early during the development of the partitioner, V3Split was failing to // split large always blocks (with ~100K assignments) so we had to handle // very large vertices with ~100K incoming and outgoing edges. // // The partitioner attempts to deal with such densely connected // graphs. Some of the tuning parameters below reference "huge vertices", // that's what they're talking about, vertices with tens of thousands of // edges in and out. Whereas most graphs have only tens of edges in and out // of most vertices. // // V3Split has since been fixed to more reliably split large always // blocks. It's kind of an open question whether the partitioner must // handle huge nodes gracefully. Maybe not! But it still can, given // appropriate tuning. // PART_SIBLING_EDGE_LIMIT (integer) // // Arbitrarily limit the number of edges on a single vertex that will be // considered when enumerating siblings, to the given value. This protects // the partitioner runtime in the presence of huge vertices. // // The sibling-merge is less important than the edge merge. (You can // totally disable the sibling merge and get halfway decent partitions; you // can't disable edge merges, those are fundamental to the process.) So, // skipping the enumeration of some siblings on a few vertices does not // have a large impact on the result of the partitioner. // // If your vertices are small, the limit (at 26) approaches a no-op. Hence // there's basically no cost to applying this limit even when we don't // expect huge vertices. // // If you don't care about partitioner runtime and you want the most // aggressive partition, set the limit very high. If you have huge // vertices, leave this as is. constexpr unsigned PART_SIBLING_EDGE_LIMIT = 26; // Don't produce more than a certain maximum number of MTasks. This helps // the TSP variable sort not to blow up (a concern for some of the tests) // and we probably don't want a huge number of MTasks in practice anyway // (50 to 100 is typical.) // // If the user doesn't give one with '--threads-max-mtasks', we'll set the // maximum # of MTasks to // (# of threads * PART_DEFAULT_MAX_MTASKS_PER_THREAD) constexpr unsigned PART_DEFAULT_MAX_MTASKS_PER_THREAD = 50; // end tunables. //###################################################################### // MTask utility classes struct MergeCandidateKey final { // Note: Structure layout chosen to minimize padding in PairingHeap<*>::Node uint64_t m_id; // Unique ID part of edge score uint64_t m_score; // Score part of ID bool operator<(const MergeCandidateKey& other) const { // First by Score then by ID, but notice that we want minimums using a max-heap, so reverse return m_score > other.m_score || (m_score == other.m_score && m_id > other.m_id); } }; // For efficiency, MergeCandidateScoreboard elements must derive from // PairingHeap::Node using MergeCandidateHeapNode = PairingHeap::Node; // Information associated with scoreboarding a merge candidate class MergeCandidate VL_NOT_FINAL : public MergeCandidateHeapNode { // Only the known subclasses can create or delete one of these friend class SiblingMC; friend class EdgeMC; // This structure is extremely hot. To save 8 bytes we pack // one bit indicating removedFromSb with the id. To save another // 8 bytes by not having a virtual function table, we implement the // few polymorphic methods over the two known subclasses explicitly, // using another bit of the id to denote the actual subtype. // By using the bottom bits for flags, we can still use < to compare IDs without masking. // <63:1> Serial number for ordering, <0> subtype (SiblingMC) static constexpr uint64_t IS_SIBLING_MASK = 1ULL << 0; static constexpr uint64_t ID_INCREMENT = 1ULL << 1; bool isSiblingMC() const { return m_key.m_id & IS_SIBLING_MASK; } // CONSTRUCTORS explicit MergeCandidate(bool isSiblingMC) { static uint64_t s_serial = 0; s_serial += ID_INCREMENT; // +ID_INCREMENT so doesn't set the special bottom bits m_key.m_id = s_serial | (isSiblingMC * IS_SIBLING_MASK); } ~MergeCandidate() = default; public: // METHODS SiblingMC* toSiblingMC(); // Instead of cast<>/as<> EdgeMC* toEdgeMC(); // Instead of cast<>/as<> bool mergeWouldCreateCycle() const; // Instead of virtual method inline void rescore(); uint64_t score() const { return m_key.m_score; } static MergeCandidate* heapNodeToElem(MergeCandidateHeapNode* nodep) { return static_cast(nodep); } }; static_assert(sizeof(MergeCandidate) == sizeof(MergeCandidateHeapNode), "Should not have a vtable"); // A pair of associated LogicMTask's that are merge candidates for sibling // contraction class SiblingMC final : public MergeCandidate { LogicMTask* const m_ap; // The higher ID MTask LogicMTask* const m_bp; // The lower ID MTask V3ListLinks m_aLinks; // List links to store instances of this class V3ListLinks m_bLinks; // List links to store instances of this class V3ListLinks& aLinks() { return m_aLinks; } V3ListLinks& bLinks() { return m_bLinks; } public: // List type to store instances of this class using AList = V3List; using BList = V3List; // CONSTRUCTORS SiblingMC(LogicMTask* ap, LogicMTask* bp); ~SiblingMC() = default; // METHODS void unlinkA(); void unlinkB(); LogicMTask* ap() const { return m_ap; } LogicMTask* bp() const { return m_bp; } bool mergeWouldCreateCycle() const; }; static_assert(!std::is_polymorphic::value, "Should not have a vtable"); // A merge candidate associated with an MTaskEdge (edge contraction candidate) class EdgeMC final : public MergeCandidate { MTaskEdge* const m_edgep; // The associated edge public: // CONSTRUCTORS explicit EdgeMC(MTaskEdge* edgep) : MergeCandidate{/* isSiblingMC: */ false} , m_edgep{edgep} {} ~EdgeMC() = default; // METHODS MTaskEdge* edgep() const { return m_edgep; } bool mergeWouldCreateCycle() const; }; static_assert(!std::is_polymorphic::value, "Should not have a vtable"); // Auxiliary data associated with each LogicMTask during Contraction, attached via // LogicMTask::userp(). Kept out of LogicMTask itself so that LogicMTask does not depend on the // MergeCandidate hierarchy (which the SiblingMC lists reference). struct MTaskContractionData final { // MTasks for which a SiblingMC exists with the owning MTask as the higher ID MTask (m_ap) std::unordered_set siblings; // SiblingMCs for which the owning MTask is the higher ID MTask (m_ap in SiblingMC) SiblingMC::AList aSiblingMCs; // SiblingMCs for which the owning MTask is the lower ID MTask (m_bp in SiblingMC) SiblingMC::BList bSiblingMCs; }; // The MTaskContractionData attached to 'mtaskp' (see LogicMTask::userp) static MTaskContractionData& mtaskData(const LogicMTask* mtaskp) { return *static_cast(mtaskp->userp()); } // The EdgeMC associated with 'edgep' while it is on the scoreboard, or nullptr otherwise. Held in // the edge's user pointer (see MTaskEdge), kept here so MTaskEdge does not depend on EdgeMC. static EdgeMC* edgeMC(const MTaskEdge* edgep) { return static_cast(edgep->userp()); } // Instead of dynamic cast SiblingMC* MergeCandidate::toSiblingMC() { return isSiblingMC() ? static_cast(this) : nullptr; } EdgeMC* MergeCandidate::toEdgeMC() { return isSiblingMC() ? nullptr : static_cast(this); } // Normally this would be a virtual function, but we save space by not having a vtable, // and we know we only have 2 possible subclasses. bool MergeCandidate::mergeWouldCreateCycle() const { return isSiblingMC() ? static_cast(this)->mergeWouldCreateCycle() : static_cast(this)->mergeWouldCreateCycle(); } static uint64_t siblingScore(const SiblingMC* sibsp) { const LogicMTask* const ap = sibsp->ap(); const LogicMTask* const bp = sibsp->bp(); const uint64_t mergedCpCostFwd = std::max(ap->critPathCost(GraphWay::FORWARD), bp->critPathCost(GraphWay::FORWARD)); const uint64_t mergedCpCostRev = std::max(ap->critPathCost(GraphWay::REVERSE), bp->critPathCost(GraphWay::REVERSE)); return mergedCpCostRev + mergedCpCostFwd + LogicMTask::stepCost(ap->cost() + bp->cost()); } static uint64_t edgeScore(const MTaskEdge* edgep) { // Score this edge. Lower is better. The score is the new local CP // length if we merge these MTasks. ("Local" means the longest // critical path running through the merged node.) const LogicMTask* const top = edgep->toMTaskp(); const LogicMTask* const fromp = edgep->fromMTaskp(); const uint64_t mergedCpCostFwd = std::max(fromp->critPathCost(GraphWay::FORWARD), top->critPathCostWithout(edgep)); const uint64_t mergedCpCostRev = std::max(fromp->critPathCostWithout(edgep), top->critPathCost(GraphWay::REVERSE)); return mergedCpCostRev + mergedCpCostFwd + LogicMTask::stepCost(fromp->cost() + top->cost()); } void MergeCandidate::rescore() { if (const SiblingMC* const sibp = toSiblingMC()) { m_key.m_score = siblingScore(sibp); } else { // Give a slight preference to sibling merges by increasing the cost of edge merges. // This biases towards sibling merges in case they are equal score with edge merges. // This avoid a central node growing while many leaves remain due to edge merges. m_key.m_score = 1 + edgeScore(static_cast(this)->edgep()); } } SiblingMC::SiblingMC(LogicMTask* ap, LogicMTask* bp) : MergeCandidate{/* isSiblingMC: */ true} , m_ap{ap} , m_bp{bp} { // Storage management depends on this UASSERT(ap->id() > bp->id(), "Should be ordered"); UDEBUGONLY(UASSERT(mtaskData(ap).siblings.count(bp), "Should be in sibling map");); mtaskData(m_ap).aSiblingMCs.linkBack(this); mtaskData(m_bp).bSiblingMCs.linkBack(this); } void SiblingMC::unlinkA() { VL_ATTR_UNUSED const size_t removed = mtaskData(m_ap).siblings.erase(m_bp); UDEBUGONLY(UASSERT(removed == 1, "Should have been in sibling set");); mtaskData(m_ap).aSiblingMCs.unlink(this); } void SiblingMC::unlinkB() { mtaskData(m_bp).bSiblingMCs.unlink(this); } // cppcheck-suppress duplInheritedMember bool SiblingMC::mergeWouldCreateCycle() const { return (LogicMTask::pathExistsFrom(m_ap, m_bp, nullptr) || LogicMTask::pathExistsFrom(m_bp, m_ap, nullptr)); } // cppcheck-suppress duplInheritedMember bool EdgeMC::mergeWouldCreateCycle() const { return LogicMTask::pathExistsFrom(m_edgep->fromMTaskp(), m_edgep->toMTaskp(), m_edgep); } // Scoreboard of MTask merge candidates. Owns the lifetime of the merge candidate objects: callers // add/remove candidates via the methods below and never allocate or free them directly. For edges // this maintains the invariant that an MTaskEdge has an associated EdgeMC (held in its userp()), // if and only if it is currently on the scoreboard. // // This is essentially a heap that can be hinted that some elements have changed keys, at which // point those elements are deferred as 'unknown' until the next 'rescore' call. We use the // generic PairingHeap, relying on its internal structure. For efficiency, the merge candidates are // themselves the heap nodes (MergeCandidate derives from PairingHeap::Node), so // a candidate can be on at most one scoreboard. class MergeCandidateScoreboard final { // TYPES using Heap = PairingHeap; using Node = Heap::Node; using Link = Heap::Link; // MEMBERS Heap m_known; // The heap of candidates with known scores Link m_unknown; // List of candidates with unknown scores // METHODS void addUnknown(MergeCandidate* nodep) { // Just prepend it to the list of unknown entries nodep->m_next.link(m_unknown.unlink()); m_unknown.linkNonNull(nodep); // We mark nodes on the unknown list by making their child pointer point to themselves nodep->m_kids.m_ptr = nodep; } // Add a freshly created candidate. Not returned by 'best' before the next 'rescore' call. void add(MergeCandidate* nodep) { addUnknown(nodep); } // Remove a candidate from the scoreboard. void remove(MergeCandidate* nodep) { if (nodep->m_kids.m_ptr == nodep) { // Node is on the unknown list, replace with next nodep->replaceWith(nodep->m_next.unlink()); return; } // Node is in the known heap, remove it m_known.remove(nodep); } public: // CONSTRUCTORS MergeCandidateScoreboard() = default; ~MergeCandidateScoreboard() = default; VL_UNCOPYABLE(MergeCandidateScoreboard); // The candidate with the best (lowest) known score, or nullptr if none have a known score. // This does not automatically 'rescore'; the caller must 'rescore' to reflect all candidates. MergeCandidate* best() const { return MergeCandidate::heapNodeToElem(m_known.max()); } // Tell the scoreboard a candidate's score may have changed. Its score becomes 'unknown' and it // will not be returned by 'best' until the next 'rescore'. void hintScoreChanged(MergeCandidate* nodep) { // If it's already in the unknown list, then nothing to do if (nodep->m_kids.m_ptr == nodep) return; // Otherwise it was in the heap, remove it m_known.remove(nodep); // Prepend it to the unknown list addUnknown(nodep); } // True if there are candidates with an unknown score bool needsRescore() const { return m_unknown; } // True if the given candidate's score is unknown static bool needsRescore(const MergeCandidate* nodep) { return nodep->m_kids.m_ptr == nodep; } // For each candidate whose score is unknown, recompute the score and add to the known heap void rescore() { for (Node *nodep = m_unknown.unlink(), *nextp; nodep; nodep = nextp) { // Pick up next nextp = nodep->m_next.ptr(); // Reset pointers nodep->m_next.m_ptr = nullptr; nodep->m_kids.m_ptr = nullptr; nodep->m_ownerpp = nullptr; // Re-compute the score of the candidate MergeCandidate::heapNodeToElem(nodep)->rescore(); // Re-insert into the heap m_known.insert(nodep); } } // Create the merge candidate for 'edgep' and add it to the scoreboard (out-of-line below) void addEdge(MTaskEdge* edgep) { UDEBUGONLY(UASSERT(!edgep->userp(), "Edge already has a merge candidate");); EdgeMC* const edgeMCp = new EdgeMC{edgep}; edgep->userp(edgeMCp); add(edgeMCp); } // Remove 'edgep's merge candidate from the scoreboard and delete it (out-of-line below) void removeEdge(MTaskEdge* edgep) { EdgeMC* const edgeMCp = edgeMC(edgep); UDEBUGONLY(UASSERT(edgeMCp, "Edge has no merge candidate");); edgep->userp(nullptr); remove(edgeMCp); VL_DO_DANGLING(delete edgeMCp, edgeMCp); } // Create a sibling merge candidate for 'ap' and 'bp' and add it to the scoreboard void addSibling(LogicMTask* ap, LogicMTask* bp) { add(new SiblingMC{ap, bp}); } // Remove sibling merge candidate 'smcp' from the scoreboard and delete it void removeSibling(SiblingMC* smcp) { remove(smcp); smcp->unlinkA(); smcp->unlinkB(); VL_DO_DANGLING(delete smcp, smcp); } }; //###################################################################### // Look at vertex costs (in one way) to form critical paths for each // vertex. template static void partInitHalfCriticalPaths(V3Graph& mTaskGraph, bool checkOnly) { constexpr GraphWay way{N_Way}; constexpr GraphWay rev = way.invert(); GraphStreamUnordered order{&mTaskGraph, way}; for (const V3GraphVertex* vertexp; (vertexp = order.nextp());) { const LogicMTask* const mtaskcp = static_cast(vertexp); LogicMTask* const mtaskp = const_cast(mtaskcp); uint64_t cpCost = 0; #if VL_DEBUG std::unordered_set relatives; #endif for (const V3GraphEdge& edge : vertexp->edges()) { #if VL_DEBUG // Run a few asserts on the initial mtask graph, // while we're iterating through... UASSERT_OBJ(edge.weight() != 0, mtaskp, "Should be no cut edges in MTask graph"); UASSERT_OBJ(relatives.find(edge.furtherp()) == relatives.end(), mtaskp, "Should be no redundant edges in MTask graph"); relatives.insert(edge.furtherp()); #endif const LogicMTask* const relativep = static_cast(edge.furtherp()); cpCost = std::max(cpCost, (relativep->critPathCost(way) + static_cast(relativep->stepCost()))); } if (checkOnly) { partCheckCachedScoreVsActual(mtaskp->critPathCost(way), cpCost); } else { mtaskp->setCritPathCost(way, cpCost); } } } // Look at vertex costs to form critical paths for each vertex. static void partInitCriticalPaths(V3Graph& mTaskGraph) { partInitHalfCriticalPaths(mTaskGraph, false); partInitHalfCriticalPaths(mTaskGraph, false); // Reset all MTaskEdges so that 'm_edges' will show correct CP numbers. // They would have been all zeroes on initial creation of the MTaskEdges. for (V3GraphVertex& vtx : mTaskGraph.vertices()) { for (V3GraphEdge& edge : vtx.outEdges()) edge.as()->resetCriticalPaths(); } } // Do an EXPENSIVE check to make sure that all incremental CP updates have // gone correctly. static void partCheckCriticalPaths(V3Graph& mTaskGraph) { partInitHalfCriticalPaths(mTaskGraph, true); partInitHalfCriticalPaths(mTaskGraph, true); for (const V3GraphVertex& vtx : mTaskGraph.vertices()) { const LogicMTask& mtask = static_cast(vtx); mtask.checkRelativesCp(); mtask.checkRelativesCp(); } } // ###################################################################### // PropagateCp template class PropagateCp final { // Propagate increasing critical path (CP) costs through a graph. // // Usage: // * Client increases the cost and/or CP at a node or small set of nodes // (often a pair in practice, eg. edge contraction.) // * Client calls PropagateCp::cpHasIncreased() one or more times. // Each call indicates that the inclusive CP of some "seed" vertex // has increased to a given value. // * NOTE: PropagateCp will neither read nor modify the cost // or CPs at the seed vertices, it only accesses and modifies // vertices wayward from the seeds. // * Client calls PropagateCp::go(). Internally, this iteratively // propagates the new CPs wayward through the graph. // // TYPES // We keep pending vertices in a heap during critical path propagation struct PendingKey final { LogicMTask* m_mtaskp; // The vertex in the heap uint64_t m_score; // The score of this entry void increase(uint64_t score) { UDEBUGONLY(UASSERT(score >= m_score, "Must increase");); m_score = score; } bool operator<(const PendingKey& other) const { if (m_score != other.m_score) return m_score < other.m_score; return *m_mtaskp < *other.m_mtaskp; } }; using PendingHeap = PairingHeap; using PendingHeapNode = typename PendingHeap::Node; // MEMBERS PendingHeap m_pendingHeap; // Heap of pending rescores // We allocate this many heap nodes at once static constexpr size_t ALLOC_CHUNK_SIZE = 128; PendingHeapNode* m_freep = nullptr; // List of free heap nodes std::vector> m_allocated; // Allocated heap nodes const bool m_slowAsserts; // Enable nontrivial asserts // Used only with slow asserts to check MTasks visited only once std::unordered_set m_seen; public: // CONSTRUCTORS explicit PropagateCp(bool slowAsserts) : m_slowAsserts{slowAsserts} {} // METHODS private: // Allocate a HeapNode for the given element PendingHeapNode* allocNode() { // If no free nodes available, then make some if (!m_freep) { // Allocate in chunks for efficiency m_allocated.emplace_back(new PendingHeapNode[ALLOC_CHUNK_SIZE]); // Set up free list pointer m_freep = m_allocated.back().get(); // Set up free list chain for (size_t i = 1; i < ALLOC_CHUNK_SIZE; ++i) { m_freep[i - 1].m_next.m_ptr = &m_freep[i]; } // Clear the next pointer of the last entry m_freep[ALLOC_CHUNK_SIZE - 1].m_next.m_ptr = nullptr; } // Free nodes are available, pick up the first one PendingHeapNode* const resultp = m_freep; m_freep = resultp->m_next.m_ptr; resultp->m_next.m_ptr = nullptr; return resultp; } // Release a heap node (make it available for future allocation) void freeNode(PendingHeapNode* nodep) { // Re-use the existing link pointers and simply prepend it to the free list nodep->m_next.m_ptr = m_freep; m_freep = nodep; } public: void cpHasIncreased(V3GraphVertex* vxp, uint64_t newInclusiveCp) { constexpr GraphWay way{N_Way}; constexpr GraphWay inv{way.invert()}; // For *vxp, whose CP-inclusive has just increased to // newInclusiveCp, iterate to all wayward nodes, update the edges // of each, and add each to m_pending if its overall CP has grown. for (V3GraphEdge& graphEdge : vxp->edges()) { MTaskEdge& edge = static_cast(graphEdge); LogicMTask* const relativep = edge.furtherMTaskp(); EdgeHeap::Node& edgeHeapNode = edge.m_edgeHeapNode[inv]; if (newInclusiveCp > edgeHeapNode.key().m_score) { relativep->m_edgeHeap[inv].increaseKey(&edgeHeapNode, newInclusiveCp); } const uint64_t critPathCost = relativep->critPathCost(way); if (critPathCost >= newInclusiveCp) continue; // relativep's critPathCost() is out of step with its longest !wayward edge. // Schedule that to be resolved. const uint64_t newVal = newInclusiveCp - critPathCost; void*& pendingNodepRef = relativep->m_propagateHeapNodep; if (PendingHeapNode* const nodep = static_cast(pendingNodepRef)) { // Already in heap. Increase score if needed. if (newVal > nodep->key().m_score) m_pendingHeap.increaseKey(nodep, newVal); continue; } // Add to heap PendingHeapNode* const nodep = allocNode(); pendingNodepRef = nodep; m_pendingHeap.insert(nodep, {relativep, newVal}); } } void go() { constexpr GraphWay way{N_Way}; constexpr GraphWay inv{way.invert()}; // m_pending maps each pending vertex to the amount that it wayward // CP will grow. // // We can iterate over the pending set in reverse order, always // choosing the nodes with the largest pending CP-growth. // // The intuition is: if the original seed node had its CP grow by // 50, the most any wayward node can possibly grow is also 50. So // for anything pending to grow by 50, we know we can process it // once and we won't have to grow its CP again on the current pass. // After we're done with all the grow-by-50s, nothing else will // grow by 50 again on the current pass, and we can process the // grow-by-49s and we know we'll only have to process each one // once. And so on. // // This generalizes to multiple seed nodes also. while (!m_pendingHeap.empty()) { // Pop max element from heap PendingHeapNode* const maxp = m_pendingHeap.max(); m_pendingHeap.remove(maxp); // Pick up values LogicMTask* const mtaskp = maxp->key().m_mtaskp; const uint64_t cpGrowBy = maxp->key().m_score; // Free the heap node, we are done with it freeNode(maxp); mtaskp->m_propagateHeapNodep = nullptr; // Update the critPathCost of mtaskp, that was out-of-date with respect to its edges const uint64_t startCp = mtaskp->critPathCost(way); const uint64_t newCp = startCp + cpGrowBy; if (VL_UNLIKELY(m_slowAsserts)) { // Check that CP matches that of the longest edge wayward of vxp. const uint64_t edgeCp = mtaskp->m_edgeHeap[inv].max()->key().m_score; UASSERT_OBJ(edgeCp == newCp, mtaskp, "CP doesn't match longest wayward edge"); // Confirm that we only set each node's CP once. That's an // important property of PropagateCp which allows it to be far // faster than a recursive algorithm on some graphs. const bool first = m_seen.insert(mtaskp).second; UASSERT_OBJ(first, mtaskp, "Set CP on node twice"); } mtaskp->setCritPathCost(way, newCp); cpHasIncreased(mtaskp, newCp + mtaskp->stepCost()); } if (VL_UNLIKELY(m_slowAsserts)) m_seen.clear(); } private: VL_UNCOPYABLE(PropagateCp); }; //###################################################################### // Contraction // Perform edge or sibling contraction on the partition graph class Contraction final { // TYPES // New CP information for mtaskp reflecting an upcoming merge struct NewCp final { uint64_t cp; uint64_t propagateCp; bool propagate; }; // MEMBERS OrderMTaskGraph& m_mTaskGraph; // The Mtask graph uint64_t m_scoreLimit; // Sloppy score allowed when picking merges // Next score rescore at uint64_t m_scoreLimitBeforeRescore = std::numeric_limits::max(); unsigned m_mergesSinceRescore = 0; // Merges since last rescore const bool m_slowAsserts{v3Global.opt.debugPartition()}; // Take extra time to validate steps MergeCandidateScoreboard m_sb; // Scoreboard // Auxiliary per-MTask data (the SiblingMC lists) attached to each MTask via its user pointer. // Owned here for the lifetime of this Contraction. A single array, as the number of MTasks is // fixed for that lifetime: merging only ever deletes vertices, never creates them. std::unique_ptr m_mtaskDatap; PropagateCp m_forwardPropagator{m_slowAsserts}; // Forward propagator PropagateCp m_reversePropagator{m_slowAsserts}; // Reverse propagator // Singular source vertex of the OrderMTaskGraph LogicMTask* const m_entryMTaskp = m_mTaskGraph.entryp(); // Singular sink vertex of the dependency graph LogicMTask* const m_exitMTaskp = m_mTaskGraph.exitp(); // Merge edges from a LogicMtask, keeping the merge candidate scoreboard in sync. static void partRedirectEdgesFrom(V3Graph& graph, LogicMTask* recipientp, LogicMTask* donorp, MergeCandidateScoreboard& sb) { // This code removes adjacent edges. When this occurs, mark it in need // of a rescore, in case its score has fallen and we need to move it up // toward the front of the scoreboard. // // Wait, what? Shouldn't the scores only increase as we merge nodes? Well // that's almost true. But there is one exception. // // Suppose we have A->B, B->C, and A->C. // // The A->C edge is a "transitive" edge. It's ineligible to be merged, as // the merge would create a cycle. We score it on the scoreboard like any // other edge. // // However, our "score" estimate for A->C is bogus, because the forward // critical path to C and the reverse critical path to A both contain the // same node (B) so we overestimate the score of A->C. At first this // doesn't matter, since transitive edges aren't eligible to merge anyway. // // Later, suppose the edge contractor decides to merge the B->C edge, with // B donating all its incoming edges into C, say. (So we reach this // function.) // // With B going away, the A->C edge will no longer be transitive and it // will become eligible to merge. But if we don't mark it for rescore, // it'll stay in the scoreboard with its old (overestimate) score. We'll // merge it too late due to the bogus score. When we finally merge it, we // fail the assert in the main edge contraction loop which checks that the // actual score did not fall below the scoreboard's score. // // Another way of stating this: this code ensures that scores of // non-transitive edges only ever increase. // Process outgoing edges while (MTaskEdge* const edgep = static_cast(donorp->outEdges().frontp())) { LogicMTask* const relativep = edgep->toMTaskp(); relativep->removeRelativeEdge(edgep); if (recipientp->hasRelativeMTask(relativep)) { // An edge already exists between recipient and relative of donor. // Mark it in need of a rescore // The donor edge is going away, so remove it from the scoreboard if (edgep->userp()) sb.removeEdge(edgep); MTaskEdge* const existMTaskEdgep = static_cast( recipientp->findConnectingEdgep(relativep)); UDEBUGONLY(UASSERT(existMTaskEdgep, "findConnectingEdge didn't find edge");); // The existing edge is no longer transitive, so may need a rescore if (EdgeMC* const existEdgeMCp = edgeMC(existMTaskEdgep)) { sb.hintScoreChanged(existEdgeMCp); } VL_DO_DANGLING(edgep->unlinkDelete(), edgep); } else { // No existing edge between recipient and relative of donor. // Redirect the edge from donor<->relative to recipient<->relative. edgep->relinkFromp(recipientp); recipientp->addRelativeMTask(relativep); recipientp->stealRelativeEdge(edgep); relativep->addRelativeEdge(edgep); // The redirected edge is a merge candidate again if (EdgeMC* const edgeMCp = edgeMC(edgep)) { sb.hintScoreChanged(edgeMCp); } else { sb.addEdge(edgep); } } } // Process incoming edges while (MTaskEdge* const edgep = static_cast(donorp->inEdges().frontp())) { LogicMTask* const relativep = edgep->fromMTaskp(); relativep->removeRelativeMTask(donorp); relativep->removeRelativeEdge(edgep); if (relativep->hasRelativeMTask(recipientp)) { // An edge already exists between recipient and relative of donor. // Mark it in need of a rescore // The donor edge is going away, so remove it from the scoreboard if (edgep->userp()) sb.removeEdge(edgep); MTaskEdge* const existMTaskEdgep = static_cast( recipientp->findConnectingEdgep(relativep)); UDEBUGONLY(UASSERT(existMTaskEdgep, "findConnectingEdge didn't find edge");); // The existing edge is no longer transitive, so may need a rescore if (EdgeMC* const existEdgeMCp = edgeMC(existMTaskEdgep)) { sb.hintScoreChanged(existEdgeMCp); } VL_DO_DANGLING(edgep->unlinkDelete(), edgep); } else { // No existing edge between recipient and relative of donor. // Redirect the edge from donor<->relative to recipient<->relative. edgep->relinkTop(recipientp); relativep->addRelativeMTask(recipientp); relativep->addRelativeEdge(edgep); recipientp->stealRelativeEdge(edgep); // The redirected edge is a merge candidate again if (EdgeMC* const edgeMCp = edgeMC(edgep)) { sb.hintScoreChanged(edgeMCp); } else { sb.addEdge(edgep); } } } // Remove donorp from the graph VL_DO_DANGLING(donorp->unlinkDelete(&graph), donorp); } template NewCp newCp(const LogicMTask* mtaskp, const LogicMTask* otherp, const MTaskEdge* mergeEdgep) { constexpr GraphWay way{N_Way}; // Return new wayward-CP for mtaskp reflecting its upcoming merge // with otherp. Set 'result.propagate' if mtaskp's wayward // relatives will see a new wayward CP from this merge. uint64_t newCp; if (mergeEdgep) { if (mtaskp == mergeEdgep->furtherp()) { newCp = std::max(otherp->critPathCost(way), mtaskp->critPathCostWithout(mergeEdgep)); } else { newCp = std::max(mtaskp->critPathCost(way), otherp->critPathCostWithout(mergeEdgep)); } } else { newCp = std::max(otherp->critPathCost(way), mtaskp->critPathCost(way)); } const uint64_t origRelativesCp = mtaskp->critPathCost(way) + mtaskp->stepCost(); const uint64_t newRelativesCp = newCp + LogicMTask::stepCost(mtaskp->cost() + otherp->cost()); NewCp result; result.cp = newCp; result.propagate = (newRelativesCp > origRelativesCp); result.propagateCp = newRelativesCp; return result; } void removeSiblingMCsWith(LogicMTask* mtaskp) { // Note: 'removeSibling' unlinks the candidate from both of its MTasks' lists, so taking // the front element repeatedly does terminate. It also erases the candidate from the // owning (higher id) MTask's sibling set as it goes, so both the sets and the lists are // left consistent, whichever side of the candidate 'mtaskp' happens to be on. while (SiblingMC* const smcp = mtaskData(mtaskp).aSiblingMCs.frontp()) { m_sb.removeSibling(smcp); } while (SiblingMC* const smcp = mtaskData(mtaskp).bSiblingMCs.frontp()) { m_sb.removeSibling(smcp); } } void removeSiblingMCs(LogicMTask* recipientp, LogicMTask* donorp) { // These two can share a SiblingMC (an edge between them does not preclude one). That is // fine: 'removeSiblingMCsWith' unlinks each candidate from both sides, so the shared one // is gone by the time we get to the donor. // // This also leaves both sibling sets empty, so they need no separate clearing: each entry // in an MTask's sibling set is added by 'makeSiblingMC' together with a SiblingMC on that // same MTask's 'aSiblingMCs' list, and draining that list erases the matching entry (see // 'SiblingMC::unlinkA'). The slow assert in 'makeSiblingMC' catches it if that ever // diverges, as a stale set entry there suppresses creating the SiblingMC it stands for. removeSiblingMCsWith(recipientp); removeSiblingMCsWith(donorp); } void contract(MergeCandidate* mergeCanp) { LogicMTask* top = nullptr; LogicMTask* fromp = nullptr; EdgeMC* const mergeEdgeMCp = mergeCanp->toEdgeMC(); MTaskEdge* const mergeEdgep = mergeEdgeMCp ? mergeEdgeMCp->edgep() : nullptr; SiblingMC* const mergeSibsp = mergeCanp->toSiblingMC(); if (mergeEdgep) { top = mergeEdgep->toMTaskp(); fromp = mergeEdgep->fromMTaskp(); } else { top = mergeSibsp->ap(); fromp = mergeSibsp->bp(); } // Merge the smaller mtask into the larger mtask. If one of them // is much larger, this will save time in partRedirectEdgesFrom(). // Assume the more costly mtask has more edges. // // [TODO: now that we have edge maps, we could count the edges // exactly without a linear search.] LogicMTask* recipientp; LogicMTask* donorp; if (fromp->cost() > top->cost()) { recipientp = fromp; donorp = top; } else { donorp = fromp; recipientp = top; } VL_DANGLING(fromp); VL_DANGLING(top); // Use donorp and recipientp now instead // Recursively update forward and reverse CP numbers. // // Doing this before merging the MTasks lets us often avoid // recursing through either incoming or outgoing edges on one or // both MTasks. // // These 'NewCp' objects carry a bit indicating whether we must // propagate CP for each of the four cases: const NewCp recipientNewCpFwd = newCp(recipientp, donorp, mergeEdgep); const NewCp donorNewCpFwd = newCp(donorp, recipientp, mergeEdgep); const NewCp recipientNewCpRev = newCp(recipientp, donorp, mergeEdgep); const NewCp donorNewCpRev = newCp(donorp, recipientp, mergeEdgep); if (mergeEdgep) { // Remove and free the connecting edge. Must do this before propagating CP's below. m_sb.removeEdge(mergeEdgep); mergeEdgep->fromMTaskp()->removeRelativeMTask(mergeEdgep->toMTaskp()); mergeEdgep->fromMTaskp()->removeRelativeEdge(mergeEdgep); mergeEdgep->toMTaskp()->removeRelativeEdge(mergeEdgep); VL_DO_DANGLING(mergeEdgep->unlinkDelete(), mergeEdgep); } else { // Remove the siblingMC m_sb.removeSibling(mergeSibsp); } // This also updates cost and stepCost on recipientp recipientp->moveAllVerticesFrom(donorp); UINFO(9, "recipient = " << recipientp->id() << ", donor = " << donorp->id() << ", mergeEdgep = " << mergeEdgep << "\n" << "recipientNewCpFwd = " << recipientNewCpFwd.cp << (recipientNewCpFwd.propagate ? " true " : " false ") << recipientNewCpFwd.propagateCp << "\n" << "donorNewCpFwd = " << donorNewCpFwd.cp << (donorNewCpFwd.propagate ? " true " : " false ") << donorNewCpFwd.propagateCp); recipientp->setCritPathCost(GraphWay::FORWARD, recipientNewCpFwd.cp); if (recipientNewCpFwd.propagate) { m_forwardPropagator.cpHasIncreased(recipientp, recipientNewCpFwd.propagateCp); } recipientp->setCritPathCost(GraphWay::REVERSE, recipientNewCpRev.cp); if (recipientNewCpRev.propagate) { m_reversePropagator.cpHasIncreased(recipientp, recipientNewCpRev.propagateCp); } if (donorNewCpFwd.propagate) { m_forwardPropagator.cpHasIncreased(donorp, donorNewCpFwd.propagateCp); } if (donorNewCpRev.propagate) { m_reversePropagator.cpHasIncreased(donorp, donorNewCpRev.propagateCp); } m_forwardPropagator.go(); m_reversePropagator.go(); // Remove all other SiblingMCs that include recipientp or donorp. We remove all siblingMCs // of recipientp so we do not get huge numbers of SiblingMCs. We'll recreate them below, up // to a bounded number. removeSiblingMCs(recipientp, donorp); // Redirect all edges, delete donorp partRedirectEdgesFrom(m_mTaskGraph, recipientp, donorp, m_sb); ++m_mergesSinceRescore; // Do an expensive check, confirm we haven't botched the CP // updates. if (m_slowAsserts) partCheckCriticalPaths(m_mTaskGraph); // Finally, make new sibling pairs as needed: // - prereqs and postreqs of recipientp // - prereqs of recipientp's postreqs // - postreqs of recipientp's prereqs // Note that this depends on the updated critical paths (above). siblingPairFromRelatives(recipientp); siblingPairFromRelatives(recipientp); unsigned edges = 0; for (V3GraphEdge& edge : recipientp->outEdges()) { LogicMTask* const postreqp = static_cast(edge.top()); siblingPairFromRelatives(postreqp); ++edges; if (edges >= PART_SIBLING_EDGE_LIMIT) break; } edges = 0; for (V3GraphEdge& edge : recipientp->inEdges()) { LogicMTask* const prereqp = static_cast(edge.fromp()); siblingPairFromRelatives(prereqp); ++edges; if (edges >= PART_SIBLING_EDGE_LIMIT) break; } } void doRescore() { // During rescore, we know that graph isn't changing, so allow // the critPathCost*Without() routines to cache some data in // each LogicMTask. This is just an optimization, things should // behave identically without the caching (just slower) m_sb.rescore(); UINFO(6, "Did rescore. Merges since previous = " << m_mergesSinceRescore); m_mergesSinceRescore = 0; m_scoreLimitBeforeRescore = std::numeric_limits::max(); } void makeSiblingMC(LogicMTask* ap, LogicMTask* bp) { if (ap->id() < bp->id()) std::swap(ap, bp); // The higher id vertex owns the association set const auto first = mtaskData(ap).siblings.insert(bp).second; if (first) { m_sb.addSibling(ap, bp); return; } if (VL_UNLIKELY(m_slowAsserts)) { // It's fine if we already have this SiblingMC, we may have // created it earlier. Just confirm that we have associated data. bool found = false; for (const SiblingMC& smc : mtaskData(ap).aSiblingMCs) { UASSERT_OBJ(smc.ap() == ap, ap, "Inconsistent SiblingMC"); if (smc.bp() == bp) found = true; } UASSERT_OBJ(found, ap, "Sibling not found"); } } template void siblingPairFromRelatives(V3GraphVertex* mtaskp) { constexpr GraphWay way{N_Way}; // Need at least 2 edges auto& edges = mtaskp->edges(); if (!edges.hasMultipleElements()) return; std::array neighbors; // This is a hot method, so we want so sort as efficiently as possible. We pre-load // all data (critical path cost and id) required for determining ordering into an aligned // structure. There is not enough space next to these to keep a whole pointer within 16 // bytes, so we store an index into the neighbors buffer instead. We can then compare // and swap these sorting records very efficiently. With this the standard library sorting // functions are efficient enough and using more optimized methods (e.g.: sorting networks) // has no measurable benefit. struct alignas(16) SortingRecord final { uint64_t m_cp; uint32_t m_id; uint8_t m_idx; static_assert(PART_SIBLING_EDGE_LIMIT <= std::numeric_limits::max(), "m_idx must fit all indices into 'neighbors'"); bool operator<(const SortingRecord& that) const { return m_cp < that.m_cp || (m_cp == that.m_cp && m_id < that.m_id); } }; static_assert(sizeof(SortingRecord) <= 16, "How could this be padded to more than 16?"); std::array sortRecs; size_t n = 0; // Populate the buffers for (V3GraphEdge& edge : mtaskp->edges()) { LogicMTask* const otherp = static_cast(edge.furtherp()); neighbors[n] = otherp; sortRecs[n].m_id = otherp->id(); sortRecs[n].m_cp = otherp->critPathCost(way) + otherp->cost(); sortRecs[n].m_idx = n; ++n; // Prevent nodes with huge numbers of edges from massively slowing down us down if (n >= PART_SIBLING_EDGE_LIMIT) break; } // Don't make all possible pairs of siblings when not requested (non-exhaustive). // Just make a few pairs. constexpr size_t MAX_NONEXHAUSTIVE_PAIRS = 3; if (N_Exhaustive || n <= 2 * MAX_NONEXHAUSTIVE_PAIRS) { const size_t end = n & ~static_cast(1); // Round down to even, (we want pairs) std::sort(sortRecs.begin(), sortRecs.begin() + n); for (size_t i = 0; i < end; i += 2) { makeSiblingMC(neighbors[sortRecs[i].m_idx], neighbors[sortRecs[i + 1].m_idx]); } } else { constexpr size_t end = 2 * MAX_NONEXHAUSTIVE_PAIRS; std::partial_sort(sortRecs.begin(), sortRecs.begin() + end, sortRecs.begin() + n); for (size_t i = 0; i < end; i += 2) { makeSiblingMC(neighbors[sortRecs[i].m_idx], neighbors[sortRecs[i + 1].m_idx]); } } } // CONSTRUCTORS Contraction(OrderMTaskGraph& mTaskGraph, uint64_t scoreLimit) : m_mTaskGraph{mTaskGraph} , m_scoreLimit{scoreLimit} { if (m_slowAsserts) { // Check there are no redundant edges for (V3GraphVertex& vtx : m_mTaskGraph.vertices()) { std::unordered_set neighbors; for (V3GraphEdge& edge : vtx.outEdges()) { const bool first = neighbors.insert(edge.top()).second; UASSERT_OBJ(first, &vtx, "Redundant edge found in input to Contraction()"); } } } // Set up the critical path into and out of each node, then coarsen the graph. partInitCriticalPaths(mTaskGraph); const uint32_t maxMTasks = []() -> uint32_t { // If specified, use the given value const int given = v3Global.opt.threadsMaxMTasks(); if (given > 0) return given; // Unspecified so estimate return PART_DEFAULT_MAX_MTASKS_PER_THREAD * v3Global.opt.threads(); }(); // OPTIMIZATION PASS: Edge contraction and sibling contraction. // - Score pairs of LogicMTask which are a candidate to merge. // * Each edge defines such a candidate pair // * Two LogicMTask that are prereqs or postreqs of a common third // vertex are "siblings", these are also a candidate pair. // - Build a list of MergeCandidates, sorted by score. // - Merge the best pair. // - Incrementally recompute critical paths near the merged mtask. // Allocate and assign the auxiliary data for every LogicMTask. { const size_t nMTasks = m_mTaskGraph.vertices().size(); m_mtaskDatap.reset(new MTaskContractionData[nMTasks]); size_t i = 0; for (V3GraphVertex& vtx : m_mTaskGraph.vertices()) vtx.userp(&m_mtaskDatap[i++]); UASSERT(i == nMTasks, "Inconsistent MTask count"); } // Add initial candidates for (V3GraphVertex& vtx : m_mTaskGraph.vertices()) { for (V3GraphEdge& edge : vtx.outEdges()) m_sb.addEdge(static_cast(&edge)); siblingPairFromRelatives(&vtx); siblingPairFromRelatives(&vtx); } // Set initial scores in scoreboard doRescore(); while (true) { // This is the best edge to merge, with the lowest score (shortest local critical path) MergeCandidate* const mergeCanp = m_sb.best(); if (!mergeCanp) { if (!m_sb.needsRescore()) break; // No more eligible candidates // Rescore the scoreboard and try again doRescore(); continue; } UASSERT(!m_sb.needsRescore(mergeCanp), "Need-rescore items should not be returned by bestp"); const uint64_t cachedScore = mergeCanp->score(); mergeCanp->rescore(); const uint64_t actualScore = mergeCanp->score(); // If cached score is out-of-date, mark this elem as in need of a rescore and continue. // cppcheck-suppress knownConditionTrueFalse // they are in fact different if (actualScore > cachedScore) { m_sb.hintScoreChanged(mergeCanp); continue; } // ... we'll also confirm that actualScore hasn't shrunk relative // to cached score, after the mergeWouldCreateCycle() check. if (actualScore > m_scoreLimit) { // Our best option isn't good enough if (m_sb.needsRescore()) { // Some pairs need a rescore, maybe those will be // eligible to merge afterward. doRescore(); continue; } // We've exhausted everything below m_scoreLimit; stop. // Except, if we have too many LogicMTasks, raise the score limit and keep going... const unsigned mtaskCount = m_mTaskGraph.vertices().size(); if (mtaskCount > maxMTasks) { const uint64_t oldLimit = m_scoreLimit; m_scoreLimit = (m_scoreLimit * 120) / 100; FileLine* const flp = v3Global.rootp()->fileline(); if (!flp->warnIsOff(V3ErrorCode::UNOPTTHREADS)) { flp->v3warn(UNOPTTHREADS, "Thread scheduler is unable to provide requested " "parallelism; suggest asking for fewer threads."); flp->modifyWarnOff(V3ErrorCode::UNOPTTHREADS, true); } UINFO(6, "Critical path limit was=" << oldLimit << " now=" << m_scoreLimit); continue; } // Really stop break; } // If time to rescore, that will result in a higher scoreLimitBeforeRescore, and // possibly lower-scoring elements returned from bestp(). if (actualScore > m_scoreLimitBeforeRescore) { doRescore(); continue; } // Avoid merging the entry/exit nodes. This would create serialization, by forcing the // merged MTask to run before/after everything else. Empirically this helps performance // in a modest way by allowing other MTasks to start earlier. if (EdgeMC* const edgeMCp = mergeCanp->toEdgeMC()) { MTaskEdge* const edgep = edgeMCp->edgep(); if (edgep->fromp() == m_entryMTaskp || edgep->top() == m_exitMTaskp) { m_sb.removeEdge(edgep); continue; } } // Avoid merging any edge that would create a cycle. // // For example suppose we begin with vertices A, B, C and edges // A->B, B->C, A->C. // // Suppose we want to merge A->C into a single vertex. // New edges would be AC->B and B->AC which is not a DAG. // Do not allow this. if (mergeCanp->mergeWouldCreateCycle()) { // Remove this candidate from scoreboard so we don't keep // reconsidering it on every loop. if (SiblingMC* const smcp = mergeCanp->toSiblingMC()) { m_sb.removeSibling(smcp); } else { m_sb.removeEdge(mergeCanp->toEdgeMC()->edgep()); } continue; } partCheckCachedScoreVsActual(cachedScore, actualScore); // Finally there's no cycle risk, no need to rescore, we're // within m_scoreLimit and m_scoreLimitBeforeRescore. // This is the edge to merge. // Bookkeeping: if this is the first edge we'll merge since // the last rescore, compute the new m_scoreLimitBeforeRescore // to be somewhat higher than this edge's score. if (!m_mergesSinceRescore) m_scoreLimitBeforeRescore = actualScore; // Finally merge this candidate. contract(mergeCanp); } // Free all remaining merge candidates. As an EdgeMC exists exactly while its edge is on // the scoreboard, draining the scoreboard here frees every remaining EdgeMC; edges removed // from the scoreboard earlier already had theirs freed. Note 'best' only ever returns // candidates with a known score, so this only drains the scoreboard completely if nothing // is left with an unknown score. Every 'break' out of the loop above is guarded on that, // but assert it here, as otherwise we would leak candidates. UASSERT(!m_sb.needsRescore(), "Should have no unknown score candidates at this point"); while (MergeCandidate* const mergeCanp = m_sb.best()) { if (SiblingMC* const smcp = mergeCanp->toSiblingMC()) { m_sb.removeSibling(smcp); } else { m_sb.removeEdge(mergeCanp->toEdgeMC()->edgep()); } } } public: static void apply(OrderMTaskGraph& mTaskGraph, uint64_t scoreLimit) { Contraction{mTaskGraph, scoreLimit}; } }; //###################################################################### // OrderMTaskGraph entry point void OrderMTaskGraph::contract(OrderMTaskGraph& mtaskGraph, uint64_t scoreLimit) { Contraction::apply(mtaskGraph, scoreLimit); }