// Copyright Advanced Micro Devices, Inc., or its affiliates. // SPDX-License-Identifier: MIT #include #include #include #include namespace rocRoller { namespace KernelGraph { namespace LoadStoreSpecification { /** * @brief Load/store specification. * * This uniquely identifies an load/store operation. */ struct Spec { /// Target coordinate (source for loads, destination /// for stores). This is a storage node: User, LDS /// etc int targetCoord; /// Parent operation. If the load is within a loop, /// this is the ForLoopOp. If not, this is -1. int parentOp; /// Operation variant. Differentiates between the /// operation types. int opType; /// Unroll colour: unroll coordinate to unroll value /// mapping. std::map unrollColour; auto operator<=>(Spec const&) const = default; }; std::string toString(Spec const& x) { std::stringstream os; os << "{"; os << " target: " << x.targetCoord; os << ", parent: " << x.parentOp; os << ", type: " << x.opType; os << ", colour: [ "; for(auto [coord, value] : x.unrollColour) { os << "(" << coord << ", " << value << "), "; } os << "] }"; return os.str(); } /** * @brief Return load/store specifier for the load/store operation. */ Spec getLoadStoreSpecifier(KernelGraph const& k, UnrollColouring const& colouring, int opTag) { auto [target, direction] = getOperationTarget(opTag, k); auto maybeForLoop = findContainingOperation(opTag, k); auto parentOp = -1; if(maybeForLoop) parentOp = *maybeForLoop; auto unrollColour = std::map{}; if(maybeForLoop && colouring.operationColour.contains(opTag)) { unrollColour = colouring.operationColour.at(opTag); } int opType = k.control.getNode(opTag).index(); return {target, parentOp, opType, unrollColour}; } } namespace RD { namespace CT = rocRoller::KernelGraph::CoordinateGraph; bool loadForLDS(int loadTag, KernelGraph const& graph) { auto dst = graph.mapper.get(loadTag); dst = only(graph.coordinates.getOutputNodeIndices(dst, CT::isEdge)) .value_or(dst); bool hasLDS = false; for(auto output : graph.coordinates.getInputNodeIndices(dst, CT::isEdge)) { if(graph.coordinates.get(output).has_value()) return true; } return false; } /** * @brief Return set of workgroup operations to consider * for deduplication. * * This looks for global loads and LDS stores that operate * on workgroup tiles. */ std::unordered_set getWGCandidates(KernelGraph const& graph) { std::unordered_set candidates; for(auto op : graph.control.getNodes()) { auto isLoadTiled = graph.control.get(op).has_value(); auto isStoreLDSTile = graph.control.get(op).has_value(); if(!(isLoadTiled || isStoreLDSTile)) continue; // Only consider within the KLoop auto maybeForLoop = findContainingOperation(op, graph); if(!maybeForLoop) continue; auto forLoop = *graph.control.get(*maybeForLoop); if(not forLoop.loopName.starts_with(rocRoller::KLOOP)) continue; // If LoadTiled, don't consider when loading directly if(isLoadTiled && !loadForLDS(op, graph)) continue; // If we get this far, the operation is a // "workgroup" operation. This implies that // jammed colours should be squashed. candidates.insert(op); } return candidates; } /** * @brief Return set of wave operations to consider for * deduplication. * * This looks for LDS loads that operate on wave tiles. */ std::unordered_set getWaveCandidates(KernelGraph const& graph) { std::unordered_set candidates; for(auto op : graph.control.getNodes()) { auto isLoadTiled = graph.control.get(op).has_value(); auto isLoadLDSTile = graph.control.get(op).has_value(); if(!(isLoadTiled || isLoadLDSTile)) continue; // Only consider within the KLoop auto maybeForLoop = findContainingOperation(op, graph); if(!maybeForLoop) continue; auto forLoop = *graph.control.get(*maybeForLoop); if(not forLoop.loopName.starts_with(rocRoller::KLOOP)) continue; // If LoadTiled, only consider when loading directly if(isLoadTiled && loadForLDS(op, graph)) continue; candidates.insert(op); } return candidates; } /** * @brief Return set of Unroll coordinates that are * neighbours of ForLoops. * * These are "jammed" coordinates. */ std::unordered_set jammedColours(KernelGraph const& graph) { std::unordered_set rv; for(auto forLoopOpTag : graph.control.getNodes()) { auto forLoopOp = *graph.control.get(forLoopOpTag); if(not forLoopOp.loopName.starts_with(rocRoller::KLOOP)) { auto [forLoopCoord, _ignore] = getForLoopCoords(forLoopOpTag, graph); auto maybeUnroll = findUnrollNeighbour(graph, forLoopCoord); if(maybeUnroll) rv.insert(*maybeUnroll); } } return rv; } /** * @brief Return mapping of "existing" operation to set of * "duplicate" operations (subset of `candidates`) that * should be removed from the control graph. * * Note that the "existing" operation is * NodeOrdering::LeftFirst of all members of the * "duplicate" set. * * An operation is a duplicate of another if: * - they are the same operations (LoadTiled, StoreLDSTile, LoadLDSTile) * - they have the same body-parent (typically ForLoopOp) * - they have the same target (User, LDS) * - they have the same unroll colouring * * If `squashJammedColours` is enabled: any colouring * associated with a JammedWaveTileNumber is squashed (set * to zero) when comparing unroll colouring. */ std::map> findDuplicates(KernelGraph const& graph, UnrollColouring const& colouring, auto const& candidates, bool squashJammedColours, std::unordered_map const& nodeOrders) { using namespace LoadStoreSpecification; std::map> duplicates; std::map opSpecs; // First pass, create specification to earliest-operation mapping for(auto op : candidates) { auto spec = getLoadStoreSpecifier(graph, colouring, op); if(opSpecs.contains(spec)) { auto originalOp = opSpecs[spec]; if(nodeOrders.at(originalOp) < nodeOrders.at(op)) opSpecs[spec] = op; } else { opSpecs[spec] = op; } } std::unordered_set squashColours; if(squashJammedColours) squashColours = jammedColours(graph); // Second pass, mark duplicates for(auto op : candidates) { auto spec = getLoadStoreSpecifier(graph, colouring, op); auto originalSpec = spec; if(!squashColours.empty()) { for(auto kv : originalSpec.unrollColour) { if(squashColours.contains(kv.first)) originalSpec.unrollColour[kv.first] = 0; } } if(opSpecs.contains(originalSpec)) { auto originalOp = opSpecs[originalSpec]; if(op != originalOp) { duplicates[originalOp].insert(op); Log::debug("Marking as duplicate: {} (orignal {}) {}", op, originalOp, toString(spec)); } } } return duplicates; } /** * @brief Remove duplicates from the graph. * * The operation (either the original operation, or its * top-most SetCoordinate) is replaced by a NOP. * * References to duplicate tiles are updated to the * non-duplicate tiles. */ KernelGraph removeDuplicates(KernelGraph const& original, auto const& duplicates) { auto graph = original; GraphReindexer expressionReindexer; for(auto [originalOp, duplicateOps] : duplicates) { auto topOriginalOp = getTopSetCoordinate(graph, originalOp); for(auto duplicateOp : duplicateOps) { auto topDuplicateOp = getTopSetCoordinate(graph, duplicateOp); // Replace top-duplicate with a NOP, then purge the top-duplicate. // // Recall that (see findDuplicates) any // operation we replace with a NOP happens // *after* the existing operation that it // duplicates. auto nop = graph.control.addElement(ControlGraph::NOP()); replaceWith(graph, topDuplicateOp, nop, false); purgeNodeAndChildren(graph, topDuplicateOp); Log::debug("RemoveDuplicates: NOPing {} (top of {}) because it duplicates " "{} (top of {}). NOP is {}.", topDuplicateOp, duplicateOp, topOriginalOp, originalOp, nop); // Mark for update: references to the // duplicate tile need to be updated to the // original tile. auto duplicateTileTag = original.mapper.get(duplicateOp); if(duplicateTileTag != -1) { auto originalTileTag = original.mapper.get(originalOp); expressionReindexer.coordinates.emplace(duplicateTileTag, originalTileTag); } } } // Collapse reindexing chains in expressionReindexer.coordinates. // If there are mappings like A -> B and B -> C, ensure A maps directly to C. // This prevents intermediate mappings from leaving dangling references. for(auto& [tag, mappedTag] : expressionReindexer.coordinates) { while(expressionReindexer.coordinates.contains(mappedTag) and mappedTag != tag) { auto nextTag = expressionReindexer.coordinates.at(mappedTag); if(nextTag == mappedTag) break; mappedTag = nextTag; } // Update the mapping to point directly to the final target. expressionReindexer.coordinates[tag] = mappedTag; } // Update tile references auto kernel = *graph.control.roots().begin(); reindexExpressions(graph, kernel, expressionReindexer); // Update tile connections and remove old tiles for(auto [tag, mappedTag] : expressionReindexer.coordinates) { auto connections = graph.mapper.getCoordinateConnections(tag); graph.mapper.purgeMappingsTo(tag); for(auto conn : connections) { conn.coordinate = mappedTag; graph.mapper.connect(conn); } AssertFatal(mappedTag == tag || graph.mapper.getCoordinateConnections(tag).empty()); if(graph.mapper.getCoordinateConnections(tag).empty()) { for(auto const& child : graph.coordinates.getLocation(tag).incoming) graph.coordinates.deleteElement(child); for(auto const& child : graph.coordinates.getLocation(tag).outgoing) graph.coordinates.deleteElement(child); graph.coordinates.deleteElement(tag); } } return graph; } } template static void collectOrderedNodes(KernelGraph const& graph, std::unordered_set& visited, int tag, std::vector& result) { // cppcheck-suppress internalAstError auto traverse = [&]() { for(auto child : graph.control.getOutputNodeIndices(tag)) { if(not visited.contains(child)) { visited.insert(child); collectOrderedNodes(graph, visited, child, result); } } }; traverse.template operator()(); traverse.template operator()(); traverse.template operator()(); traverse.template operator()(); traverse.template operator()(); bool const isTargetOperation = (graph.control.get(tag).has_value() || ...); if(isTargetOperation) { result.push_back(tag); } } template std::vector getOrderedNodes(KernelGraph const& graph) { auto roots = graph.control.roots().to(); std::vector result; if(roots.empty()) return result; std::unordered_set visited; for(auto const& node : roots) { visited.insert(node); collectOrderedNodes(graph, visited, node, result); } return result; } KernelGraph RemoveDuplicates::apply(KernelGraph const& original) { auto colouring = colourByUnrollValue(original); auto graph = original; removeRedundantSequenceEdges(graph); auto orderedNodes = getOrderedNodes(graph); std::unordered_map nodeOrders; for(int i = 0; i < orderedNodes.size(); i++) nodeOrders.emplace(orderedNodes[i], i); // Two passes: // 1. Workgroup (LoadTiled and StoreLDSTile) and // 2. Wave (LoadLDSTile) // // We assume that these passes are independent of each // other. In particular, the first pass doesn't change // the colouring etc that is used for the second pass. // LoadTile and StoreLDSTile operate on Workgroup tiles Log::debug("RemoveDuplicates Workgroup pass"); { auto candidates = RD::getWGCandidates(original); auto duplicates = RD::findDuplicates(original, colouring, candidates, true, nodeOrders); graph = RD::removeDuplicates(graph, duplicates); } // LoadLDSTile operation on Wave tiles Log::debug("RemoveDuplicates Wave pass"); { auto candidates = RD::getWaveCandidates(original); auto duplicates = RD::findDuplicates(original, colouring, candidates, false, nodeOrders); graph = RD::removeDuplicates(graph, duplicates); } removeRedundantNOPs(graph); return graph; } } }