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

860 lines
38 KiB
C++

// Copyright Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
#include <rocRoller/CommandSolution.hpp>
#include <rocRoller/Expression.hpp>
#include <rocRoller/KernelGraph/ControlGraph/ControlFlowRWTracer.hpp>
#include <rocRoller/KernelGraph/KernelGraph.hpp>
#include <rocRoller/KernelGraph/Transforms/Simplify.hpp>
#include <rocRoller/KernelGraph/Transforms/UnrollLoops.hpp>
#include <rocRoller/KernelGraph/Utils.hpp>
#include <rocRoller/KernelGraph/Visitors.hpp>
namespace rocRoller
{
namespace KernelGraph
{
using GD = Graph::Direction;
using namespace ControlGraph;
using namespace CoordinateGraph;
unsigned int
getUnrollAmount(KernelGraph& graph, int loopTag, CommandParametersPtr const& params)
{
auto name = getForLoopName(graph, loopTag);
// Only attempt to unroll some loops for now...
auto onlyUnroll
= std::set<std::string>{rocRoller::XLOOP, rocRoller::YLOOP, rocRoller::KLOOP};
if(!onlyUnroll.contains(name))
return 1u;
auto dimTag = graph.mapper.get(loopTag, NaryArgument::DEST);
auto forLoopLength = getSize(graph.coordinates.getNode(dimTag));
auto unrollK = params->unrollK;
// K-loop uses the unrollK parameter if specified
if(name == rocRoller::KLOOP && unrollK > 0)
return unrollK;
// X and Y loops always get fully unrolled if loop length is a constant
if(name == rocRoller::XLOOP || name == rocRoller::YLOOP)
{
if(Expression::evaluationTimes(
forLoopLength)[Expression::EvaluationTime::Translate])
{
auto length = Expression::evaluate(forLoopLength);
if(isInteger(length))
return std::max(1u, getUnsignedInt(length));
}
}
return 1u;
}
/**
* @brief Find loop-carried dependencies.
*
* If a coordinate:
*
* 1. is written to only once
* 2. is read-after-write
* 3. has the loop coordinate in it's coordinate transform
* for the write
*
* then we take a giant leap of faith and say that it
* doesn't have loop-carried-dependencies.
*
* Returns a map with a coordinate node as a key and a list of
* every control node that uses that coordinate node as its value.
*/
std::map<int, std::vector<int>> findLoopCarriedDependencies(KernelGraph const& kgraph,
int forLoop)
{
using RW = ControlFlowRWTracer::ReadWrite;
enum class RAW
{
INVALID,
NAUGHT,
WRITE,
READ,
Count
};
Log::debug("KernelGraph::findLoopDependencies({})", forLoop);
auto topForLoopCoord = kgraph.mapper.get<ForLoop>(forLoop);
ControlFlowRWTracer tracer(kgraph, forLoop);
auto readwrite = tracer.coordinatesReadWrite();
// Coordinates that are read/write in loop body
std::unordered_set<int> coordinates;
for(auto m : readwrite)
coordinates.insert(m.coordinate);
// Coordinates with loop-carried-dependencies; true by
// default.
std::unordered_set<int> loopCarriedDependencies;
for(auto m : readwrite)
loopCarriedDependencies.insert(m.coordinate);
// Now we want to determine which coordinates don't have
// loop carried dependencies...
//
// If a coordinate:
//
// 1. is written to only once
// 2. is read-after-write
// 3. has the loop coordinate in it's coordinate transform
// for the write
//
// then we take a giant leap of faith and say that it
// doesn't have loop-carried-dependencies.
for(auto coordinate : coordinates)
{
std::optional<int> writeOperation;
// Is coordinate RAW?
//
// RAW finite state machine moves through:
// NAUGHT -> WRITE upon write
// WRITE -> READ upon read
// READ -> READ upon read
// -> INVALID otherwise
//
// If it ends in READ, we have a RAW
RAW raw = RAW::NAUGHT;
for(auto x : readwrite)
{
if(x.coordinate != coordinate)
continue;
// Note: "or raw == RAW::WRITE" is absent on purpose;
// only handle single write for now
if((raw == RAW::NAUGHT) && (x.rw == RW::WRITE || x.rw == RW::READWRITE))
{
raw = RAW::WRITE;
writeOperation = x.control;
continue;
}
if((raw == RAW::WRITE || raw == RAW::READ) && (x.rw == RW::READ))
{
raw = RAW::READ;
continue;
}
raw = RAW::INVALID;
}
if(raw != RAW::READ)
continue;
AssertFatal(writeOperation);
// Does the coordinate transform for the write
// operation contain the loop iteration?
auto [required, path] = findAllRequiredCoordinates(*writeOperation, kgraph);
if(required.empty())
{
// Local variable
loopCarriedDependencies.erase(coordinate);
continue;
}
auto forLoopCoords = filterCoordinates<ForLoop>(required, kgraph);
for(auto forLoopCoord : forLoopCoords)
if(forLoopCoord == topForLoopCoord)
loopCarriedDependencies.erase(coordinate);
}
// For the loop-carried-depencies, find the write operation.
std::map<int, std::vector<int>> result;
for(auto coordinate : loopCarriedDependencies)
{
for(auto x : readwrite)
{
if(x.coordinate != coordinate)
continue;
if(x.rw == RW::WRITE || x.rw == RW::READWRITE)
result[coordinate].push_back(x.control);
}
}
return result;
}
/**
* @brief Make operations sequential.
*
* Make operations in `sequentialOperations` execute
* sequentially. This changes concurrent patterns similar to
*
* Kernel/Loop/Scope
* | |
* ... ...
* | |
* OperA OperB
* | |
* ... ...
*
* into a sequential pattern
*
* Kernel/Loop/Scope
* | |
* ... ...
* | |
* SetCoord --->SetCoord ---> remaining
* | |
* OperA OperB
*
*/
void makeSequential(KernelGraph& graph,
const std::vector<std::vector<int>>& sequentialOperations)
{
for(int i = 0; i < sequentialOperations.size() - 1; i++)
{
auto a = getTopSetCoordinate(graph, sequentialOperations[i].back());
auto b = getTopSetCoordinate(graph, sequentialOperations[i + 1].front());
auto edge = graph.control.addElement(Sequence(), {a}, {b});
Log::debug("UnrollLoops::makeSequential:: Added Sequence edge {}", edge);
}
}
void buildControlStack(KernelGraph& graph,
int const tag,
std::unordered_set<int>& visited,
std::vector<int>& controlStack,
bool const bodyParent)
{
// cppcheck-suppress syntaxError
auto const traverseEdge = [&]<typename EdgeType>() {
for(auto parent : graph.control.getInputNodeIndices<EdgeType>(tag))
{
if(visited.contains(parent))
continue;
visited.insert(parent);
buildControlStack(
graph, parent, visited, controlStack, !std::is_same_v<EdgeType, Sequence>);
}
};
traverseEdge.template operator()<Body>();
traverseEdge.template operator()<Else>();
traverseEdge.template operator()<Sequence>();
if(bodyParent)
controlStack.push_back(tag);
}
void orderCurrentAndPreviousNodes(KernelGraph& graph,
std::optional<std::pair<int, int>>& previousNodes,
std::set<int>& currentNodes)
{
if(currentNodes.empty())
return;
auto [firstNode, lastNode] = getFirstAndLastNodes(graph, currentNodes);
if(previousNodes.has_value())
{
auto A = std::get<1>(previousNodes.value());
auto B = firstNode;
std::vector<int> controlStackA;
std::unordered_set<int> visited;
buildControlStack(graph, A, visited, controlStackA, true);
std::vector<int> controlStackB;
visited.clear();
buildControlStack(graph, B, visited, controlStackB, true);
graph.control.orderMemoryNodes(controlStackA, controlStackB, true);
}
previousNodes = std::make_pair(firstNode, lastNode);
}
/**
* @brief Add an Unroll dimension beside the ForLoop dimension.
*/
int addUnrollDimension(KernelGraph& graph, int forLoopDimension, int unrollAmount)
{
auto forLoopLocation = graph.coordinates.getLocation(forLoopDimension);
auto unrollDimension = graph.coordinates.addElement(Unroll(unrollAmount));
for(auto const& input : forLoopLocation.incoming)
{
auto edge = graph.coordinates.getEdge(input);
auto isCT = std::holds_alternative<CoordinateTransformEdge>(edge);
if(not isCT)
continue;
auto insertUnrollBesideForLoop = [&](auto edgeType) {
auto parent
= only(graph.coordinates.getNeighbours<GD::Upstream>(input)).value();
auto children = graph.coordinates.getNeighbours<GD::Downstream>(input);
children.insert(std::find(children.begin(), children.end(), forLoopDimension)
+ 1,
unrollDimension);
graph.coordinates.addElement(edgeType, std::vector<int>{parent}, children);
graph.coordinates.deleteElement(input);
};
std::visit(
overloaded{
[](Identify const&) {},
[&](PassThrough const&) {
int parent
= *graph.coordinates.getNeighbours<GD::Upstream>(input).begin();
graph.coordinates.addElement(
Split(), {parent}, {forLoopDimension, unrollDimension});
graph.coordinates.deleteElement(input);
},
[&](Tile const&) { insertUnrollBesideForLoop(Tile()); },
[&](Split const&) { insertUnrollBesideForLoop(Split()); },
[&](auto const&) {
Throw<FatalError>("Unhandled incoming edge while creating Unroll.",
ShowValue(input));
},
},
std::get<CoordinateTransformEdge>(edge));
}
for(auto const& output : forLoopLocation.outgoing)
{
auto edge = graph.coordinates.getEdge(output);
auto isCT = std::holds_alternative<CoordinateTransformEdge>(edge);
if(not isCT)
continue;
auto insertUnrollBesideForLoop = [&](auto edgeType) {
auto child
= only(graph.coordinates.getNeighbours<GD::Downstream>(output)).value();
auto parents = graph.coordinates.getNeighbours<GD::Upstream>(output);
parents.insert(std::find(parents.begin(), parents.end(), forLoopDimension) + 1,
unrollDimension);
graph.coordinates.addElement(edgeType, parents, std::vector<int>{child});
graph.coordinates.deleteElement(output);
};
std::visit(
overloaded{
[](Identify const&) {},
[](Forget const&) {},
[&](PassThrough const&) {
int child
= *graph.coordinates.getNeighbours<GD::Downstream>(output).begin();
graph.coordinates.addElement(
Join(), {forLoopDimension, unrollDimension}, {child});
graph.coordinates.deleteElement(output);
},
[&](Flatten const&) { insertUnrollBesideForLoop(Flatten()); },
[&](Join const&) { insertUnrollBesideForLoop(Join()); },
[&](auto const&) {
Throw<FatalError>("Unhandled outgoing edge while creating Unroll.",
ShowValue(output));
},
},
std::get<CoordinateTransformEdge>(edge));
}
auto otherForLoopDim
= only(graph.coordinates.getOutputNodeIndices(forLoopDimension, isEdge<Identify>));
if(otherForLoopDim)
{
Log::debug("Adding Unroll Identify from {} to {}",
unrollDimension,
otherForLoopDim.value());
graph.coordinates.addElement(
Identify(), {unrollDimension}, {otherForLoopDim.value()});
}
return unrollDimension;
}
UnrollLoops::UnrollLoops(CommandParametersPtr params, ContextPtr context)
: m_params(params)
, m_context(context)
{
}
void UnrollLoops::unrollLoop(KernelGraph& graph, int tag)
{
if(m_unrolledLoopOps.count(tag) > 0)
{
Log::debug(" Unrolled loop {} already, skipping.", tag);
return;
}
m_unrolledLoopOps.insert(tag);
auto bodies = graph.control.getOutputNodeIndices<Body>(tag).to<std::vector>();
{
// Unroll contained loops first.
auto traverseBodies = graph.control.depthFirstVisit(bodies).to<std::vector>();
for(const auto node : traverseBodies)
{
if(graph.control.exists(node)
&& isOperation<ForLoopOp>(graph.control.getElement(node)))
{
unrollLoop(graph, node);
}
}
}
auto unrollAmount = getUnrollAmount(graph, tag, m_params);
Log::debug(" Unrolling loop {}, amount {}", tag, unrollAmount);
if(unrollAmount <= 1)
return;
auto forLoopDimension = graph.mapper.get<ForLoop>(tag);
AssertFatal(forLoopDimension >= 0,
"Unable to find ForLoop dimension for " + std::to_string(tag));
int unrollDimension = createUnrollDimension(graph, forLoopDimension, unrollAmount);
{
auto tailLoop
= createTailLoop(graph, tag, unrollAmount, unrollDimension, forLoopDimension);
if(tailLoop)
m_unrolledLoopOps.insert(*tailLoop);
}
std::map<int, std::vector<int>> loopCarriedDependencies;
// TODO: Iron out storage node duplication
//
// If we aren't doing the KLoop: some assumptions made
// during subsequent lowering stages (LDS) are no
// longer satisifed.
//
// For now, to avoid breaking those assumptions, only
// follow through with loop-carried analysis when we
// are doing the K loop.
//
// To disable loop-carried analysis, just leave the
// dependencies as an empty list...
std::unordered_set<int> dontDuplicate;
if(getForLoopName(graph, tag) != rocRoller::KLOOP)
{
dontDuplicate = graph.coordinates.getNodes<LDS>().to<std::unordered_set>();
}
else
{
loopCarriedDependencies = findLoopCarriedDependencies(graph, tag);
for(auto const& [coord, controls] : loopCarriedDependencies)
{
// In the KLOOP, we do want to duplicate LDS (and paired tiles)
bool pairedWithLDS = false;
for(auto op : controls)
pairedWithLDS |= graph.mapper.get<LDS>(op) != -1;
if(pairedWithLDS)
continue;
dontDuplicate.insert(coord);
}
}
// Change the loop increment calculation
// Multiply the increment amount by the unroll amount
// Find the ForLoopIcrement calculation
// TODO: Handle multiple ForLoopIncrement edges that might be in a different format.
auto loopIncrement = graph.control.getOutputNodeIndices<ForLoopIncrement>(tag).only();
AssertFatal(loopIncrement.has_value(), "Should only have 1 loop increment edge");
auto loopIncrementOp = graph.control.getNode<Assign>(loopIncrement.value());
auto [lhs, rhs] = getForLoopIncrement(graph, tag);
auto newAddExpr = lhs + (rhs * Expression::literal(unrollAmount));
loopIncrementOp.expression = newAddExpr;
graph.control.setElement(*loopIncrement, loopIncrementOp);
// Update size of ForLoop coordinate
{
auto [forLoopCoordTag, _ignore] = getForLoopCoords(tag, graph);
auto forLoopCoord = graph.coordinates.get<ForLoop>(forLoopCoordTag).value();
forLoopCoord.size = forLoopCoord.size / Expression::literal(unrollAmount);
graph.coordinates.setElement(forLoopCoordTag, forLoopCoord);
}
// Add a setCoordinate node in between the original ForLoopOp and the loop bodies
// Delete edges between original ForLoopOp and original loop body
for(auto const& child : graph.control.getNeighbours<GD::Downstream>(tag))
{
if(isEdge<Body>(graph.control.getElement(child)))
{
graph.control.deleteElement(child);
}
}
// Function for adding a SetCoordinate nodes around all load and
// store operations. Only adds a SetCoordinate node if the coordinate is needed
// by the load/store operation.
auto connectWithSetCoord = [&](std::vector<int> const& toConnect,
unsigned int coordValue) -> std::vector<int> {
std::vector<int> rv;
for(auto const& body : toConnect)
{
graph.control.addElement(Body(), {tag}, {body});
for(auto const& op : findIndexAssignmentCandidates(graph, body))
{
auto pendingOp = op;
auto [required, path] = findAllRequiredCoordinates(op, graph);
if(path.contains(unrollDimension))
{
if(!hasExistingSetCoordinate(graph, op, coordValue, unrollDimension))
{
auto name = getForLoopName(graph, tag);
if(name == rocRoller::XLOOP)
graph.mapper.connect<Unroll>(
op, unrollDimension, rocRoller::XLOOP_UNROLL);
else if(name == rocRoller::YLOOP)
graph.mapper.connect<Unroll>(
op, unrollDimension, rocRoller::YLOOP_UNROLL);
else if(name == rocRoller::KLOOP)
graph.mapper.connect<Unroll>(
op, unrollDimension, rocRoller::KLOOP_UNROLL);
auto setCoord = replaceWith(graph,
op,
graph.control.addElement(SetCoordinate(
Expression::literal(coordValue))),
false);
graph.mapper.connect<Unroll>(setCoord, unrollDimension);
graph.control.addElement(Body(), {setCoord}, {op});
pendingOp = setCoord;
Log::debug(
"Added SetCoordinate {} for coordinate {} (value {}) above "
"operation {}",
setCoord,
unrollDimension,
coordValue,
op);
}
}
rv.push_back(pendingOp);
}
}
return rv;
};
// Create duplicates of the loop body and populate the sequentialOperations
// data structure. sequentialOperations is a map that uses a coordinate with
// a loop carried dependency as a key and contains a vector of vectors
// with every control node that uses that coordinate in each new body of the loop.
std::vector<std::vector<int>> duplicatedBodies;
std::map<int, std::vector<std::vector<int>>> sequentialOperations;
for(unsigned int i = 0; i < unrollAmount; i++)
{
if(m_unrollReindexers.count({forLoopDimension, i}) == 0)
m_unrollReindexers[{forLoopDimension, i}] = std::make_shared<GraphReindexer>();
auto unrollReindexer = m_unrollReindexers[{forLoopDimension, i}];
auto dontDuplicatePredicate = [&](int x) { return dontDuplicate.contains(x); };
if(i == 0)
duplicatedBodies.push_back(bodies);
else
duplicatedBodies.push_back(duplicateControlNodes(
graph, unrollReindexer, bodies, dontDuplicatePredicate));
for(auto const& [coord, controls] : loopCarriedDependencies)
{
std::vector<int> dupControls;
for(auto const& control : controls)
{
if(i == 0)
{
dupControls.push_back(control);
}
else
{
auto dupOp = unrollReindexer->control.at(control);
dupControls.push_back(dupOp);
}
}
sequentialOperations[coord].emplace_back(std::move(dupControls));
}
unrollReindexer->control = {};
}
// Connect the duplicated bodies to the loop, adding SetCoordinate nodes
// as needed.
auto isLoadTiled = graph.control.isElemType<LoadTiled>();
auto isLoadLDSTile = graph.control.isElemType<LoadLDSTile>();
auto isStoreTiled = graph.control.isElemType<StoreTiled>();
auto isStoreLDSTile = graph.control.isElemType<StoreLDSTile>();
auto getTops = [&](auto predicate, auto starts) {
std::set<int> rv;
for(auto op :
filter(predicate, graph.control.depthFirstVisit(starts, GD::Downstream)))
{
rv.insert(getTopSetCoordinate(graph, op));
}
return rv;
};
Log::debug(" Ordering loop {}", tag);
std::optional<std::pair<int, int>> previousLoads;
std::optional<std::pair<int, int>> previousLDSLoads;
std::optional<std::pair<int, int>> previousStores;
std::optional<std::pair<int, int>> previousLDSStores;
auto name = getForLoopName(graph, tag);
for(int i = 0; i < unrollAmount; i++)
{
duplicatedBodies[i] = connectWithSetCoord(duplicatedBodies[i], i);
auto currentLoads = getTops(isLoadTiled, duplicatedBodies[i]);
auto currentLDSLoads = getTops(isLoadLDSTile, duplicatedBodies[i]);
auto currentStores = getTops(isStoreTiled, duplicatedBodies[i]);
auto currentLDSStores = getTops(isStoreLDSTile, duplicatedBodies[i]);
orderCurrentAndPreviousNodes(graph, previousLoads, currentLoads);
orderCurrentAndPreviousNodes(graph, previousLDSLoads, currentLDSLoads);
orderCurrentAndPreviousNodes(graph, previousStores, currentStores);
orderCurrentAndPreviousNodes(graph, previousLDSStores, currentLDSStores);
currentLDSLoads
= filter(isLoadLDSTile,
graph.control.depthFirstVisit(duplicatedBodies[i], GD::Downstream))
.to<std::set>();
for(auto ldsLoad : currentLDSLoads)
{
if(name == rocRoller::KLOOP)
graph.mapper.connect<Unroll>(
ldsLoad, unrollDimension, rocRoller::KLOOP_UNROLL);
}
}
// If there are any loop carried dependencies, add Sequence nodes
// between the control nodes with dependencies.
Log::debug(" Adding sequence edges between loop-carried-dependencies {}", tag);
for(auto [coord, allControls] : sequentialOperations)
{
makeSequential(graph, allControls);
}
}
// Add Unroll dimension to the coordinates graph and return it.
int UnrollLoops::createUnrollDimension(KernelGraph& graph,
int forLoopDimension,
int unrollAmount)
{
if(not m_unrolledLoopDimensions.contains(forLoopDimension))
m_unrolledLoopDimensions[forLoopDimension]
= addUnrollDimension(graph, forLoopDimension, unrollAmount);
return m_unrolledLoopDimensions[forLoopDimension];
}
/**
* If needed/appropriate, create a tail loop. Will return the node ID of
* the tail loop, if created, otherwise `nullopt`.
*
* Will not create a tail loop if:
* - The loop has a known trip count, which is divisible by unrollAmount
* - Tail loops are manually disabled.
*
* - Duplicate the for loop, making the duplicate after the original
* (sequence edge).
* - Change the original loop's limit to round down to a multiple of
* unrollAmount ((x / unrollAmount) * unrollAmount).
* - Change the new loop to start at that value.
*
* -------------------------
* Before:
*
* Other --seq--> SetCoord --seq--> Other
* |
* Body
* |
* v
* ForLoop
*
* -------------------------
* After:
*
* /-------------------seq--------------|
* | v
* Other --seq--> SetCoord --seq--> SetCoord --seq--> Other
* | |
* Body Body
* | |
* v v
* ForLoop ForLoop
* (original) (tail)
*/
std::optional<int> UnrollLoops::createTailLoop(KernelGraph& graph,
int loop,
int unrollAmount,
int unrollDimension,
int forLoopDimension)
{
if(!m_params->tailLoops)
{
Log::debug("Not adding tail loop for {} because tail loops are disabled.", loop);
return std::nullopt;
}
if(getForLoopName(graph, loop) != rocRoller::KLOOP)
{
Log::debug("Not adding tail loop for {} because it is {}, not {}.",
loop,
getForLoopName(graph, loop),
KLOOP);
return std::nullopt;
}
auto loopOp = graph.control.getNode<ForLoopOp>(loop);
auto [loopVariable, loopSize] = split<Expression::LessThan>(loopOp.condition);
auto isTranslateTime = evaluationTimes(loopSize)[Expression::EvaluationTime::Translate];
if(isTranslateTime)
{
auto vis = rocRoller::overloaded{
[&](std::integral auto val) { return val % unrollAmount == 0; },
[](auto val) { return false; }};
bool divisibleByUnroll = std::visit(vis, evaluate(loopSize));
if(divisibleByUnroll)
{
Log::debug(
"Not adding tail loop for {} because the size ({}) is divisible by {}",
loop,
toString(loopSize),
unrollAmount);
return std::nullopt;
}
}
auto loopSizeType = resultVariableType(loopSize);
auto amount = Expression::literal(unrollAmount, loopSizeType);
auto loopSizeRoundedDown = (loopSize / amount) * amount;
Log::debug("Adding tail loop for {}. Size {} -> {}",
loop,
toString(loopSize),
toString(loopSizeRoundedDown));
auto tailLoop = cloneForLoop(graph, loop, rocRoller::KLOOPTAIL);
// Set original loop to end at a multiple of the unroll amount.
{
auto newCondition = loopVariable < loopSizeRoundedDown;
copyComment(newCondition, loopOp.condition);
loopOp.condition = newCondition;
graph.control.setElement(loop, loopOp);
}
// Modify the init of the tail loop to start at the remainder.
{
auto init = graph.control.getOutputNodeIndices<Initialize>(tailLoop).only();
AssertFatal(init.has_value(),
"There must be exactly one loop initialization node.");
auto initOp = graph.control.getNode<Assign>(init.value());
{
auto isZero
= rocRoller::overloaded{[](std::integral auto val) { return val == 0; },
[](auto val) { return false; }};
AssertFatal(std::visit(isZero, evaluate(initOp.expression)),
"Init op to loop must equal 0.");
}
initOp.expression = loopSizeRoundedDown;
graph.control.setElement(*init, initOp);
}
// Duplicate the body of the original for loop into the tail.
// Follow similar logic as unrollLoop for the KLOOP, but don't duplicate LDS coordinates:
// - Find loop-carried dependencies
// - Don't duplicate coordinates that are loop-carried but NOT paired with LDS
// - Duplicate everything else (except LDS coordinates)
// This allows register-allocated coordinates to be freed before the tail loop.
{
auto loopBodies = graph.control.getOutputNodeIndices<Body>(loop).to<std::vector>();
auto loopCarriedDependencies = findLoopCarriedDependencies(graph, loop);
std::unordered_set<int> dontDuplicate;
for(auto const& [coord, controls] : loopCarriedDependencies)
{
bool pairedWithLDS = false;
for(auto op : controls)
pairedWithLDS |= graph.mapper.get<LDS>(op) != -1;
if(pairedWithLDS)
continue;
dontDuplicate.insert(coord);
}
for(auto const tag : graph.coordinates.getNodes<LDS>().to<std::unordered_set>())
{
dontDuplicate.insert(tag);
}
auto dontDuplicatePredicate = [&](int x) { return dontDuplicate.contains(x); };
auto newBodies
= duplicateControlNodes(graph, nullptr, loopBodies, dontDuplicatePredicate);
for(auto node : newBodies)
{
graph.control.chain<Body>(tailLoop, node);
}
}
{
// Add SetCoordinate for the loop size for the tail loop.
auto setCoordK = graph.control.addElement(SetCoordinate(loopSizeRoundedDown));
graph.mapper.connect<ForLoop>(setCoordK, forLoopDimension);
// Set the value of the unroll dimension to 0 for the tail loop.
auto setCoord = graph.control.addElement(SetCoordinate(Expression::literal(0u)));
graph.mapper.connect<Unroll>(setCoord, unrollDimension);
// Connect them: setCoordK -> setCoord -> tailLoop
graph.control.chain<Body>(setCoordK, setCoord, tailLoop);
// Connect the parent loop's Sequence children as Sequence children of the
// new SetCoordinate. This ensures that the tail loop is before the
// original Sequence children to the parent loop.
for(auto node : graph.control.getOutputNodeIndices<Sequence>(loop))
graph.control.chain<Sequence>(setCoord, node);
graph.control.chain<Sequence>(loop, setCoordK);
}
auto const coordValue = 0u;
auto const bodies
= graph.control.getOutputNodeIndices<Body>(tailLoop).to<std::vector>();
for(auto const body : bodies)
{
for(auto const op : findIndexAssignmentCandidates(graph, body))
{
auto [_, path] = findAllRequiredCoordinates(op, graph);
if(path.contains(unrollDimension))
{
if(!hasExistingSetCoordinate(graph, op, coordValue, unrollDimension))
{
auto setCoord = replaceWith(graph,
op,
graph.control.addElement(SetCoordinate(
Expression::literal(coordValue))),
false);
graph.mapper.connect<Unroll>(setCoord, unrollDimension);
graph.mapper.connect<Unroll>(
op, unrollDimension, rocRoller::KLOOP_UNROLL);
graph.control.chain<Body>(setCoord, op);
}
}
}
}
return tailLoop;
}
void UnrollLoops::commit(KernelGraph& kgraph)
{
for(const auto node :
kgraph.control.depthFirstVisit(kgraph.control.roots().only().value())
.to<std::vector>())
{
if(kgraph.control.exists(node)
&& isOperation<ForLoopOp>(kgraph.control.getElement(node)))
{
unrollLoop(kgraph, node);
}
}
}
KernelGraph UnrollLoops::apply(KernelGraph const& original)
{
auto graph = original;
commit(graph);
return graph;
}
}
}