Files
TheRock/rocm-libraries/shared/rocroller/lib/source/KernelGraph/Transformations/RemoveDuplicates.cpp
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

480 lines
20 KiB
C++

// Copyright Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
#include <rocRoller/KernelGraph/KernelGraph.hpp>
#include <rocRoller/KernelGraph/Transforms/RemoveDuplicates.hpp>
#include <rocRoller/KernelGraph/Transforms/Simplify.hpp>
#include <rocRoller/KernelGraph/Utils.hpp>
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<int, int> 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<ControlGraph::ForLoopOp>(opTag, k);
auto parentOp = -1;
if(maybeForLoop)
parentOp = *maybeForLoop;
auto unrollColour = std::map<int, int>{};
if(maybeForLoop && colouring.operationColour.contains(opTag))
{
unrollColour = colouring.operationColour.at(opTag);
}
int opType = k.control.getNode<ControlGraph::Operation>(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<CT::MacroTile>(loadTag);
dst = only(graph.coordinates.getOutputNodeIndices(dst, CT::isEdge<CT::Duplicate>))
.value_or(dst);
bool hasLDS = false;
for(auto output :
graph.coordinates.getInputNodeIndices(dst, CT::isEdge<CT::DataFlow>))
{
if(graph.coordinates.get<CT::LDS>(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<int> getWGCandidates(KernelGraph const& graph)
{
std::unordered_set<int> candidates;
for(auto op : graph.control.getNodes())
{
auto isLoadTiled = graph.control.get<ControlGraph::LoadTiled>(op).has_value();
auto isStoreLDSTile
= graph.control.get<ControlGraph::StoreLDSTile>(op).has_value();
if(!(isLoadTiled || isStoreLDSTile))
continue;
// Only consider within the KLoop
auto maybeForLoop = findContainingOperation<ControlGraph::ForLoopOp>(op, graph);
if(!maybeForLoop)
continue;
auto forLoop = *graph.control.get<ControlGraph::ForLoopOp>(*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<int> getWaveCandidates(KernelGraph const& graph)
{
std::unordered_set<int> candidates;
for(auto op : graph.control.getNodes())
{
auto isLoadTiled = graph.control.get<ControlGraph::LoadTiled>(op).has_value();
auto isLoadLDSTile
= graph.control.get<ControlGraph::LoadLDSTile>(op).has_value();
if(!(isLoadTiled || isLoadLDSTile))
continue;
// Only consider within the KLoop
auto maybeForLoop = findContainingOperation<ControlGraph::ForLoopOp>(op, graph);
if(!maybeForLoop)
continue;
auto forLoop = *graph.control.get<ControlGraph::ForLoopOp>(*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<int> jammedColours(KernelGraph const& graph)
{
std::unordered_set<int> rv;
for(auto forLoopOpTag : graph.control.getNodes<ControlGraph::ForLoopOp>())
{
auto forLoopOp = *graph.control.get<ControlGraph::ForLoopOp>(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<int, std::unordered_set<int>>
findDuplicates(KernelGraph const& graph,
UnrollColouring const& colouring,
auto const& candidates,
bool squashJammedColours,
std::unordered_map<int, int> const& nodeOrders)
{
using namespace LoadStoreSpecification;
std::map<int, std::unordered_set<int>> duplicates;
std::map<Spec, int> 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<int> 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<CT::MacroTile>(duplicateOp);
if(duplicateTileTag != -1)
{
auto originalTileTag = original.mapper.get<CT::MacroTile>(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 <typename... OperationTypes>
static void collectOrderedNodes(KernelGraph const& graph,
std::unordered_set<int>& visited,
int tag,
std::vector<int>& result)
{
// cppcheck-suppress internalAstError
auto traverse = [&]<typename EdgeType>() {
for(auto child : graph.control.getOutputNodeIndices<EdgeType>(tag))
{
if(not visited.contains(child))
{
visited.insert(child);
collectOrderedNodes<OperationTypes...>(graph, visited, child, result);
}
}
};
traverse.template operator()<ControlGraph::Sequence>();
traverse.template operator()<ControlGraph::ForLoopIncrement>();
traverse.template operator()<ControlGraph::Else>();
traverse.template operator()<ControlGraph::Body>();
traverse.template operator()<ControlGraph::Initialize>();
bool const isTargetOperation
= (graph.control.get<OperationTypes>(tag).has_value() || ...);
if(isTargetOperation)
{
result.push_back(tag);
}
}
template <typename... OperationTypes>
std::vector<int> getOrderedNodes(KernelGraph const& graph)
{
auto roots = graph.control.roots().to<std::vector>();
std::vector<int> result;
if(roots.empty())
return result;
std::unordered_set<int> visited;
for(auto const& node : roots)
{
visited.insert(node);
collectOrderedNodes<OperationTypes...>(graph, visited, node, result);
}
return result;
}
KernelGraph RemoveDuplicates::apply(KernelGraph const& original)
{
auto colouring = colourByUnrollValue(original);
auto graph = original;
removeRedundantSequenceEdges(graph);
auto orderedNodes = getOrderedNodes<ControlGraph::LoadTiled,
ControlGraph::StoreLDSTile,
ControlGraph::LoadLDSTile>(graph);
std::unordered_map<int, int> 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;
}
}
}