Files
TheRock/rocm-libraries/shared/rocroller/lib/source/KernelGraph/Transformations/Simplify.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

532 lines
17 KiB
C++

// Copyright Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
#include <map>
#include <queue>
#include <unordered_set>
#include <vector>
#include <rocRoller/Graph/GraphUtilities.hpp>
#include <rocRoller/KernelGraph/KernelGraph.hpp>
#include <rocRoller/KernelGraph/Transforms/Simplify.hpp>
#include <rocRoller/Utilities/Timer.hpp>
namespace rocRoller::KernelGraph
{
namespace CF = rocRoller::KernelGraph::ControlGraph;
using GD = rocRoller::Graph::Direction;
using namespace ControlGraph;
/**
* @brief Remove redundant Sequence edges in the control graph.
*
* Consider a control graph similar to:
*
* @
* |\ B
* A | \
* | @
* | /
* |/ C
* @
*
* where @ are operations and all edges are Sequence edges. The A
* edge is redundant.
*/
void removeRedundantSequenceEdges(KernelGraph& graph)
{
removeRedundantSequenceEdges(graph.control);
}
void removeRedundantSequenceEdges(rocRoller::KernelGraph::ControlGraph::ControlGraph& graph)
{
TIMER(t, "removeRedundantSequenceEdges");
std::unordered_map<int, std::unordered_set<int>> predecessors;
std::unordered_map<int, std::vector<int>> successors;
for(auto const node : graph.getNodes())
{
predecessors[node];
successors[node];
for(auto const parent : graph.getInputNodeIndices<Sequence>(node))
{
if(predecessors.at(node).contains(parent))
{
//
// Two nodes are connected by more than one (sequence) edge.
// Each pair of nodes should be connected by at most one
// (sequence) edge after the outer for-loop finishes.
//
graph.deleteElement<Sequence>(std::vector<int>{parent}, std::vector<int>{node});
}
else
predecessors.at(node).insert(parent);
}
}
for(auto const& [k, pred] : predecessors)
for(auto const& p : pred)
successors.at(p).push_back(k);
std::vector<int> roots;
for(auto const& [k, pred] : predecessors)
{
if(pred.empty())
roots.push_back(k);
}
//
// Calculate depth of each node
//
std::unordered_map<int, int> depth;
std::queue<int> readyQueue;
std::unordered_set<int> visited;
for(auto r : roots)
{
depth[r] = 0;
readyQueue.push(r);
visited.insert(r);
}
std::unordered_map<int, int> hit;
while(not readyQueue.empty())
{
auto node = readyQueue.front();
readyQueue.pop();
for(auto successor : successors.at(node))
{
if(visited.contains(successor))
continue;
depth[successor] = std::max(depth[successor], depth.at(node) + 1);
hit[successor]++;
if(hit.at(successor) == predecessors.at(successor).size())
{
readyQueue.push(successor);
visited.insert(successor);
}
}
}
// TODO: sorting in unnecessary as we just want to know the maximum depth among successors
for(auto& [node, children] : successors)
std::sort(children.begin(), children.end(), [&](int a, int b) {
return depth.at(a) < depth.at(b);
});
std::vector<int> round = std::move(roots);
std::vector<int> next_round;
std::unordered_map<int, int> seen;
// TODO: might use a queue to replace the two buffers (round and next_round)
while(!round.empty())
{
for(auto node : round)
{
//
// Skip leaves as they have no successors
//
if(successors.at(node).empty())
continue;
//
// Check the node's successors to see if a successor can be reached (seen)
// more than once. If true, the sequence edge between the node and that
// successor is redundant and can be removed.
//
readyQueue = {}; // no clear method
visited.clear();
seen.clear();
for(auto successor : successors.at(node))
{
readyQueue.push(successor);
seen[successor]++;
visited.insert(successor);
}
//
// Start traversal until reaching nodes that are below
// successors
//
int const max_depth = depth.at(successors.at(node).back());
while(not readyQueue.empty())
{
auto const node = readyQueue.front();
readyQueue.pop();
for(auto successor : successors.at(node))
{
if(depth.at(successor) <= max_depth)
{
seen[successor]++;
if(not visited.contains(successor))
{
readyQueue.push(successor);
visited.insert(successor);
}
}
}
}
//
// Sequence edges to the successors that have been seen
// more than once are redundant.
//
for(auto const successor : successors.at(node))
{
if(seen.at(successor) > 1)
{
graph.deleteElement<Sequence>(std::vector<int>{node},
std::vector<int>{successor});
}
}
//
// Insert successors that are ready (all its parents are
// visited) for next round
//
for(auto const successor : successors.at(node))
{
auto const remaining = (--hit.at(successor));
if(remaining == 0)
next_round.push_back(successor);
}
}
round.swap(next_round);
next_round.clear();
}
}
/**
* @brief Remove redundant Body edges in the control graph.
*
* Consider a control graph similar to:
*
* @
* |\ B
* A | \
* | @
* | /
* |/ C
* @
*
* where @ are operations; the A and B edges are Body edges, and
* the C edge is a Sequence edge. The A edge is redundant.
*
* If there were another Body edge parallel to A, it would also be
* redundant and should be removed.
*/
void removeRedundantBodyEdges(KernelGraph& graph)
{
removeRedundantBodyEdges(graph.control);
}
void removeRedundantBodyEdges(rocRoller::KernelGraph::ControlGraph::ControlGraph& graph)
{
TIMER(t, "removeRedundantBodyEdges");
//
// The idea is to use depth to identify redundant Body edges, where
// "depth" means the longest path from root to the node.
//
// For example,
//
// N0
// @
// |\ (Body)
// (Body)| \
// | @ N1
// | /
// |/ (Sequence)
// @
// N2
//
// In this case, N0 happens to be the root node (i.e., depth(N0)=0),
// and depth(N1) = 1, depth(N2) = 2. So edge N0-N2 is redundant.
//
// Notice that this approach assumes a Body node can only have
// Sequence edge coming from another Body node with the same
// parent. The approach will produce incorrect results if this
// assumption does not hold.
//
std::unordered_map<int, std::unordered_set<int>> predecessors;
std::unordered_map<int, std::unordered_set<int>> successors;
std::unordered_map<int, std::unordered_set<int>> bodyPredecessors;
for(auto const node : graph.getNodes())
{
bodyPredecessors[node];
for(auto const parent : graph.getInputNodeIndices<Body>(node))
{
if(bodyPredecessors.at(node).contains(parent))
{
//
// Two nodes are connected by more than one (Body) edge.
// Each pair of nodes should be connected by at most one
// (Body) edge after the outer for-loop finishes.
//
graph.deleteElement<Body>(std::vector<int>{parent}, std::vector<int>{node});
}
else
bodyPredecessors.at(node).insert(parent);
}
predecessors[node];
successors[node];
for(auto const parent : graph.getInputNodeIndices<Sequence>(node))
{
predecessors.at(node).insert(parent);
successors[parent].insert(node);
}
}
//
// Include nodes via Body edges as successors (and predecessors)
//
for(auto const& [node, bPredecessors] : bodyPredecessors)
{
for(auto pred : bPredecessors)
{
successors.at(pred).insert(node);
predecessors.at(node).insert(pred);
}
}
std::unordered_map<int, int> depth;
std::queue<int> readyQueue;
std::unordered_set<int> visited;
for(auto const& [node, pred] : predecessors)
{
if(pred.empty())
{
depth[node] = 0;
readyQueue.push(node);
visited.insert(node);
}
}
//
// Calculate the depth of each node
//
std::unordered_map<int, int> hit;
while(not readyQueue.empty())
{
auto node = readyQueue.front();
readyQueue.pop();
for(auto successor : successors.at(node))
{
if(visited.contains(successor))
continue;
depth[successor] = std::max(depth[successor], depth.at(node) + 1);
hit[successor]++;
if(hit.at(successor) == predecessors.at(successor).size())
{
readyQueue.push(successor);
visited.insert(successor);
}
}
}
//
// Check nodes via Body edges. If depth(node) != depth(predecessor) + 1, the Body
// edge between the node and its predecessor is redundant and can be deleted.
//
for(auto const& [node, bPredecessors] : bodyPredecessors)
{
for(auto pred : bPredecessors)
{
AssertFatal(depth.at(pred) < depth.at(node),
"node's predecessor should have smaller depth");
if(depth.at(pred) + 1 != depth.at(node))
graph.deleteElement<Body>(std::vector<int>{pred}, std::vector<int>{node});
}
}
}
/*
* Helper for removeRedundantNOPs (early return).
*
* Remove NOPs that have multiple inputs/outputs (MIMO) if they are redundant
*
* Returns true if it modified the graph; false otherwise.
*/
static bool removeMIMONOPIfRedundant(KernelGraph& graph, int nop)
{
auto hasBody = !empty(graph.control.getOutputNodeIndices<Body>(nop));
if(hasBody)
return false;
//
// Can we reach all output nodes from each input node?
//
auto inputs = graph.control.getInputNodeIndices(nop, [](ControlEdge) { return true; })
.to<std::vector>();
auto outputs = graph.control.getOutputNodeIndices(nop, [](ControlEdge) { return true; })
.to<std::vector>();
auto dontGoThroughNOP = [&](int x) -> bool {
auto tail = *only(graph.control.getNeighbours<GD::Upstream>(x));
auto head = *only(graph.control.getNeighbours<GD::Downstream>(x));
return (nop != tail) && (nop != head);
};
for(auto input : inputs)
{
auto reachable = graph.control.depthFirstVisit(input, dontGoThroughNOP, GD::Downstream)
.to<std::unordered_set>();
for(auto output : outputs)
{
if(!reachable.contains(output))
return false;
}
}
// If we get here, we can reach all outputs from each input
// without going through the NOP. Delete it!
auto incoming = only(graph.control.getNeighbours<GD::Upstream>(nop));
auto outgoing = only(graph.control.getNeighbours<GD::Downstream>(nop));
if((!incoming) || (!outgoing))
return false;
Log::debug("Deleting redundant NOP: {}", nop);
graph.control.deleteElement(*incoming);
graph.control.deleteElement(*outgoing);
graph.control.deleteElement(nop);
return true;
}
/*
* Helper for removeRedundantNOPs (early return).
*
* Remove NOPs that have single input/output (SISO) if they are redundant
*
* Returns true if it modified the graph; false otherwise.
*/
static bool removeSISONOPIfRedundant(KernelGraph& graph, int nop)
{
auto hasBody = !empty(graph.control.getOutputNodeIndices<Body>(nop));
if(hasBody)
return false;
//
// If the NOP has a single incoming Sequence edge, it's redundant.
//
auto singleIncomingSequence = only(graph.control.getInputNodeIndices<Sequence>(nop));
if(singleIncomingSequence)
{
auto inNode = *singleIncomingSequence;
auto incomingEdge = only(graph.control.getNeighbours<GD::Upstream>(nop));
if(!incomingEdge)
return false;
auto outgoingEdges = graph.control.getNeighbours<GD::Downstream>(nop);
for(auto outEdge : outgoingEdges)
{
auto outNode = *only(graph.control.getNeighbours<GD::Downstream>(outEdge));
graph.control.addElement(Sequence(), {inNode}, {outNode});
}
Log::debug("Deleting single-incoming NOP: {}", nop);
graph.control.deleteElement(*incomingEdge);
for(auto outgoingEdge : outgoingEdges)
graph.control.deleteElement(outgoingEdge);
graph.control.deleteElement(nop);
return true;
}
//
// If the NOP has a single outgoing Sequence edge, it's redundant.
//
auto singleOutgoingSequence = only(graph.control.getOutputNodeIndices<Sequence>(nop));
if(singleOutgoingSequence)
{
auto outNode = *singleOutgoingSequence;
auto outgoingEdge = only(graph.control.getNeighbours<GD::Downstream>(nop));
if(!outgoingEdge)
return false;
auto incomingEdges = graph.control.getNeighbours<GD::Upstream>(nop);
for(auto inEdge : incomingEdges)
{
auto edgeType = graph.control.getElement(inEdge);
auto inNode = *only(graph.control.getNeighbours<GD::Upstream>(inEdge));
graph.control.addElement(edgeType, {inNode}, {outNode});
}
Log::debug("Deleting single-outgoing NOP: {}", nop);
graph.control.deleteElement(*outgoingEdge);
for(auto incomingEdge : incomingEdges)
graph.control.deleteElement(incomingEdge);
graph.control.deleteElement(nop);
return true;
}
return false;
}
/**
* @brief Remove redundant NOP nodes.
*/
void removeRedundantNOPs(KernelGraph& graph)
{
removeRedundantSequenceEdges(graph);
auto const removeNOPFcns = {removeSISONOPIfRedundant, removeMIMONOPIfRedundant};
for(auto const& fcn : removeNOPFcns)
{
bool graphChanged = true;
while(graphChanged)
{
graphChanged = false;
auto nops = graph.control.getNodes<NOP>().to<std::vector>();
for(auto nop : nops)
graphChanged |= fcn(graph, nop);
if(graphChanged)
{
removeRedundantSequenceEdges(graph);
}
}
}
}
// Remove removeRedundantNOPs is not fully tested.
//
// Note: if we remove enough NOPs, some edges might become
// redundant. Do fixed-point iterations, or is that too slow?
KernelGraph Simplify::apply(KernelGraph const& original)
{
auto graph = original;
removeRedundantSequenceEdges(graph);
removeRedundantBodyEdges(graph);
return graph;
}
std::string Simplify::name() const
{
return "Simplify";
}
}