// -*- mode: C++; c-file-style: "cc-mode" -*- //************************************************************************* // DESCRIPTION: Verilator: Multi-threaded code partitioning and ordering // // 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 // //************************************************************************* // // Parallel code ordering // //************************************************************************* #include "V3PchAstNoMT.h" // VL_MT_DISABLED_CODE_UNIT #include "V3Ast.h" #include "V3AstUserAllocator.h" #include "V3Control.h" #include "V3Error.h" #include "V3ExecGraph.h" #include "V3Graph.h" #include "V3GraphStream.h" #include "V3OrderCFuncEmitter.h" #include "V3OrderInternal.h" #include "V3OrderMTaskGraph.h" #include #include #include #include VL_DEFINE_DEBUG_FUNCTIONS; //###################################################################### // Data hazard checker // Reports read-write and write-write pairs on the same variable that // are not ordered in the MTask graph. static void checkDataHazards(OrderMTaskGraph& mTaskGraph) { // Expensive, so only with '--debug-partition' if (!mTaskGraph.slowAsserts()) return; // Order MTasks by their stable ids, so the report is deterministic struct MTaskIdLessThan final { bool operator()(const LogicMTask* ap, const LogicMTask* bp) const { return *ap < *bp; } }; struct VarInfo final { bool m_seen = false; // Variable already appended to 'vscps' // How each MTask accesses the variable, merged over the logic within that MTask std::map m_byMTask; }; // AstVarScope::user1 -> VarInfo instance for the variable (via 'varInfos') const VNUser1InUse user1InUse; AstUser1Allocator varInfos; // The variables accessed (in enumerated order, for stability). std::vector vscps; // Gather how each MTask accesses each variable for (V3GraphVertex& vtx : mTaskGraph.vertices()) { LogicMTask& mtask = static_cast(vtx); for (const OrderMoveVertex& mVtx : mtask.vertexList()) { const OrderLogicVertex* const lVtxp = mVtx.logicp(); if (!lVtxp) continue; // A variable vertex, which performs no access itself for (const OrderLogicVertex::VarAccess& acc : lVtxp->varAccesses()) { AstVarScope* const vscp = acc.m_vscp; VarInfo& varInfo = varInfos(vscp); if (!varInfo.m_seen) { varInfo.m_seen = true; vscps.push_back(vscp); } const auto pair = varInfo.m_byMTask.emplace(&mtask, acc.m_access); // Merge the access kinds if this MTask already accessed this variable if (!pair.second && pair.first->second != acc.m_access) { pair.first->second = VAccess::READWRITE; } } } } // Report every unordered pair of accessors where at least one side writes AstVarScope* firstHazardp = nullptr; // First variable with a hazard, for the error below for (AstVarScope* const vscp : vscps) { const auto& byMTask = varInfos(vscp).m_byMTask; for (auto aIt = byMTask.begin(); aIt != byMTask.end(); ++aIt) { for (auto bIt = std::next(aIt); bIt != byMTask.end(); ++bIt) { // Concurrent reads are not a hazard if (aIt->second.isReadOnly() && bIt->second.isReadOnly()) continue; LogicMTask* const ap = aIt->first; LogicMTask* const bp = bIt->first; if (mTaskGraph.pathExists(ap, bp, nullptr)) continue; if (mTaskGraph.pathExists(bp, ap, nullptr)) continue; // LCOV_EXCL_START if (!firstHazardp) firstHazardp = vscp; UINFO(0, "Data hazard: " << vscp->name() << " " << aIt->second.ascii() << " by mt" << ap->id() << ", " << bIt->second.ascii() << " by mt" << bp->id() << " (unordered)"); // LCOV_EXCL_STOP } } } // Fail if any hazards were found if (firstHazardp) firstHazardp->v3fatalSrc("Data hazards found"); // LCOV_EXCL_BR_LINE } //###################################################################### // Partitioner implementation // Partitioner takes the fine-grained OrderMoveGraph from V3Order and collapses // it into a coarse-grained graph of LogicMTask's, each of which contains of set // of the logic nodes from the fine-grained graph. static std::unique_ptr partition(OrderMoveGraph& moveGraph) { // Build the initial MTask graph. Initially, each MTask just wraps one OrderMoveVertex. We will // merge MTasks together and eventually each MTask will wrap a large number of OrderMoveVertex // (and the logic nodes therein). std::unique_ptr mTaskGraphp = OrderMTaskGraph::build(moveGraph); mTaskGraphp->hashGraphDebug("initial MTask graph"); // Add edges to eliminate data hazards OrderMTaskGraph::fixDataHazards(*mTaskGraphp); mTaskGraphp->hashGraphDebug("MTask graph after fixDataHazards()"); // Merge MTask nodes together, repeatedly, until the critical path budget is reached. Coarsens // the graph, usually by several orders of magnitude. Some tests disable this for stability, // it should always be enabled in production. if (v3Global.opt.threadsCoarsen()) { const int nThreads = v3Global.opt.threads(); UASSERT(nThreads >= 2, "Should not reach Partitioner when --threads <= 1"); // Set critical path limit to roughly totalGraphCost / nThreads. Actually set it slighly // lower, by a hardcoded fudge factor. This results in a smaller graph, which helps reduce // fragmentation when scheduling them. TODO: What does this sentence mean? const uint64_t fudgeNum = 3; const uint64_t fudgeDen = 5; const uint64_t limit = (mTaskGraphp->totalCost() * fudgeNum) / (nThreads * fudgeDen); UINFO(4, "Partitioner set critical path limit = " << limit); OrderMTaskGraph::contract(*mTaskGraphp, limit); mTaskGraphp->hashGraphDebug("MTask graph after contract()"); } // Remove MTasks that have no logic in them, rerouting the edges mTaskGraphp->removeEmptyMTasks(); mTaskGraphp->hashGraphDebug("MTask graph after removeEmptyMTasks()"); // Note this is OrderMTaskGraph::removeTransitiveEdges, which maintains graph consistency mTaskGraphp->removeTransitiveEdges(); mTaskGraphp->hashGraphDebug("MTask graph after removeTransitiveEdges()"); // Check for data hazards the partitioning left unordered checkDataHazards(*mTaskGraphp); // Set OrderMoveVertex::userp to indicate the mtask it is part of. moveGraph.userClearVertices(); for (V3GraphVertex* const vtxp : mTaskGraphp->vertices().unlinkable()) { LogicMTask* const mtaskp = vtxp->as(); OrderMoveVertex::List& vertexList = mtaskp->vertexList(); while (OrderMoveVertex* const mVtxp = vertexList.unlinkFront()) mVtxp->userp(mtaskp); } // Return the resulting MTask graph return mTaskGraphp; } //###################################################################### // DpiThreadsVisitor - Finds number of threads used by an ExecMTask class DpiThreadsVisitor final : public VNVisitorConst { int m_threads = 1; // Max number of threads used by this mtask // METHODS void visit(AstCFunc* nodep) override { m_threads = std::max(m_threads, V3Control::getHierWorkers(nodep->cname())); iterateChildrenConst(nodep); } void visit(AstNodeCCall* nodep) override { iterateConst(nodep->funcp()); } void visit(AstNode* nodep) override { iterateChildrenConst(nodep); } // CONSTRUCTORS explicit DpiThreadsVisitor(AstCFunc* nodep) { iterateConst(nodep); } ~DpiThreadsVisitor() override = default; VL_UNCOPYABLE(DpiThreadsVisitor); public: // Number of threads occupied by the given MTask static int apply(const ExecMTask* mTaskp) { return DpiThreadsVisitor{mTaskp->funcp()}.m_threads; } }; //###################################################################### // Entry point AstNodeStmt* V3Order::createParallel(OrderMoveGraph& moveGraph, const std::string& tag, bool slow) { UINFO(2, " Constructing parallel code for '" + tag + "'"); // For nondeterminism debugging moveGraph.hashGraphDebug("V3Order::createParallel input OrderMoveGraph"); moveGraph.orderGraph().hashGraphDebug("V3Order::createParallel input OrderGraph"); // Partition moveGraph into LogicMTask's. The partitioner will set userp() on each logic // vertex in the moveGraph to the MTask it belongs to. const std::unique_ptr mTaskGraphp = partition(moveGraph); if (dumpGraphLevel() >= 9) moveGraph.dumpDotFilePrefixed(tag + "_ordermv_mtasks"); // Some variable OrderMoveVertices are not assigned to an MTask. Reroute and delete these. for (V3GraphVertex* const vtxp : moveGraph.vertices().unlinkable()) { OrderMoveVertex* const mVtxp = vtxp->as(); if (!mVtxp->userp()) { UASSERT_OBJ(!mVtxp->logicp(), mVtxp, "Logic OrderMoveVertex not assigned to mtask"); mVtxp->rerouteEdges(&moveGraph); VL_DO_DANGLING(mVtxp->unlinkDelete(&moveGraph), mVtxp); } } // Remove all edges from the move graph that cross between MTasks. Add logic to MTask lists. for (V3GraphVertex& vtx : moveGraph.vertices()) { OrderMoveVertex* const mVtxp = vtx.as(); LogicMTask* const mtaskp = static_cast(mVtxp->userp()); // Add to list in MTask, in MoveGraph order. This should not be necessary, but see #4993. mtaskp->vertexList().linkBack(mVtxp); // Remove edges crossing between MTasks for (V3GraphEdge* const edgep : mVtxp->outEdges().unlinkable()) { const OrderMoveVertex* const toMVtxp = edgep->top()->as(); if (mtaskp != toMVtxp->userp()) VL_DO_DANGLING(edgep->unlinkDelete(), edgep); } } if (dumpGraphLevel() >= 9) moveGraph.dumpDotFilePrefixed(tag + "_ordermv_pruned"); // Create the AstExecGraph node which represents the execution of the MTask graph. FileLine* const flp = v3Global.rootp()->fileline(); AstScope* const scopep = v3Global.rootp()->topScopep()->scopep(); AstExecGraph* const execGraphp = new AstExecGraph{flp, tag}; V3Graph* const depGraphp = execGraphp->depGraphp(); // Translate the LogicMTask graph into the corresponding ExecMTask graph, // which will outlive ordering. std::unordered_map logicMTaskToExecMTask; OrderMoveGraphSerializer serializer{moveGraph}; V3OrderCFuncEmitter emitter{tag, slow}; // Sort LogicMTask vertices by their serial IDs. struct MTaskVxIdLessThan final { bool operator()(const V3GraphVertex* lhsp, const V3GraphVertex* rhsp) const { return lhsp->as()->id() < rhsp->as()->id(); } }; GraphStream mtaskStream{mTaskGraphp.get()}; while (const V3GraphVertex* const vtxp = mtaskStream.nextp()) { const LogicMTask* const cMTaskp = vtxp->as(); LogicMTask* const mTaskp = const_cast(cMTaskp); // The entry and exit vertices only anchor the graph, they hold no logic and // must not become ExecMTasks. if (mTaskp == mTaskGraphp->entryp() || mTaskp == mTaskGraphp->exitp()) { UASSERT_OBJ(mTaskp->vertexList().empty(), mTaskp, "Entry and exit vertices should have no logic"); continue; } // Add initially ready vertices within this MTask to the serializer as seeds, // and unlink them from the vertex list in the MTask as we go. (The serializer // uses the list links in the vertex, so must unlink it here.) while (OrderMoveVertex* const mVtxp = mTaskp->vertexList().unlinkFront()) { if (mVtxp->inEmpty()) serializer.addSeed(mVtxp); } // Emit all logic within the MTask as they become ready OrderMoveDomScope* prevDomScopep = nullptr; while (OrderMoveVertex* const mVtxp = serializer.getNext()) { // We only really care about logic vertices if (OrderLogicVertex* const logicp = mVtxp->logicp()) { // Force a new function if the domain or scope changed, for better combining. OrderMoveDomScope* const domScopep = &mVtxp->domScope(); if (domScopep != prevDomScopep) emitter.forceNewFunction(); prevDomScopep = domScopep; // Emit the logic under this vertex emitter.emitLogic(logicp); } // Can delete the vertex now VL_DO_DANGLING(mVtxp->unlinkDelete(&moveGraph), mVtxp); } // Create the ExecMTask ExecMTask* const execMTaskp = new ExecMTask{execGraphp, scopep, emitter.getStmts()}; if (!v3Global.opt.hierBlocks().empty()) { execMTaskp->threads(DpiThreadsVisitor::apply(execMTaskp)); } const bool newEntry = logicMTaskToExecMTask.emplace(mTaskp, execMTaskp).second; UASSERT_OBJ(newEntry, mTaskp, "LogicMTasks should be processed in dependencyorder"); UINFO(3, "Final '" << tag << "' LogicMTask " << mTaskp->id() << " maps to ExecMTask" << execMTaskp->id()); // For code analysis purposes, we can pretend the AstExecGraph runs the // MTasks sequentially, in some topological order that respects edges. // The order they are created here happens to be just such an order. AstCCall* const callp = new AstCCall{flp, execMTaskp->funcp()}; callp->dtypeSetVoid(); execGraphp->addStmtsp(callp->makeStmt()); // Add the dependency edges between ExecMTasks for (const V3GraphEdge& edge : mTaskp->inEdges()) { const V3GraphVertex* fromVxp = edge.fromp(); const LogicMTask* const fromp = fromVxp->as(); // Skip the entry vertex, which has no ExecMTask if (fromp == mTaskGraphp->entryp()) continue; new V3GraphEdge{depGraphp, logicMTaskToExecMTask.at(fromp), execMTaskp, 1}; } } // Delete the remaining variable vertices for (V3GraphVertex* const vtxp : moveGraph.vertices().unlinkable()) { if (!vtxp->as()->logicp()) { VL_DO_DANGLING(vtxp->unlinkDelete(&moveGraph), vtxp); } } return execGraphp; }