Files
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

425 lines
17 KiB
C++

// Copyright Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/matchers/catch_matchers_contains.hpp>
#include <rocRoller/Expression.hpp>
#include <rocRoller/KernelGraph/ControlGraph/Operation.hpp>
#include <rocRoller/KernelGraph/CoordinateGraph/Dimension.hpp>
#include <rocRoller/KernelGraph/KernelGraph.hpp>
#include <rocRoller/KernelGraph/Transforms/All.hpp>
#include <rocRoller/KernelGraph/Transforms/HoistLoopInvariant_detail.hpp>
#include <rocRoller/KernelGraph/Utils.hpp>
#include <fstream>
#include <iostream>
#include "TestContext.hpp"
#include <common/CommonGraphs.hpp>
#include <common/Utilities.hpp>
TEST_CASE("extractDataFlowTags", "[kernel-graph][hoist-loop-invariant][expression]")
{
using namespace rocRoller;
namespace kg = rocRoller::KernelGraph;
SECTION("Binary operation with DataFlowTags")
{
Expression::DataFlowTag tag1{42, Register::Type::Vector, DataType::Float};
Expression::DataFlowTag tag2{77, Register::Type::Vector, DataType::Float};
auto tag1Ptr = std::make_shared<Expression::Expression>(tag1);
auto tag2Ptr = std::make_shared<Expression::Expression>(tag2);
auto binaryExpr = Expression::Add{{tag1Ptr, tag2Ptr}};
auto extractedTags = kg::extractDataFlowTags(binaryExpr);
REQUIRE(extractedTags.size() == 2);
REQUIRE(extractedTags.count(42) == 1);
REQUIRE(extractedTags.count(77) == 1);
}
}
TEST_CASE("hoist loop invariant", "[kernel-graph][hoist-loop-invariant]")
{
using namespace rocRoller;
using namespace rocRoller::KernelGraph;
namespace kg = rocRoller::KernelGraph;
using namespace rocRoller::KernelGraph::CoordinateGraph;
using namespace rocRoller::KernelGraph::ControlGraph;
auto context = TestContext::ForDefaultTarget();
auto example = rocRollerTest::Graphs::GEMM(DataType::Float);
int macK = 16;
int waveK = 2;
example.setTileSize(256, 64, macK);
example.setMFMA(32, 32, waveK, 1);
example.setUseLDS(true, true, false);
example.setPrefetch(true, 2, 2, false);
auto graph = example.getKernelGraph();
auto params = example.getCommandParameters();
graph = transform<IdentifyParallelDimensions>(graph);
graph = transform<OrderMemory>(graph, true);
graph = transform<UpdateParameters>(graph, params);
graph = transform<AddLDS>(graph, params, context.get());
graph = transform<LowerLinear>(graph, context.get());
graph = transform<LowerTile>(graph, params, context.get());
graph = transform<LowerTensorContraction>(graph, params, context.get());
graph = transform<Simplify>(graph);
graph = transform<ConstantPropagation>(graph);
graph = transform<FuseExpressions>(graph);
graph = transform<ConnectWorkgroups>(graph, context.get());
graph = transform<WorkgroupRemapXCC>(graph, context.get(), params->workgroupRemapXCC);
graph = transform<UnrollLoops>(graph, params, context.get());
graph = transform<FuseLoops>(graph);
graph = transform<RemoveDuplicates>(graph);
graph = transform<OrderEpilogueBlocks>(graph);
graph = transform<Simplify>(graph);
graph = transform<CleanLoops>(graph);
graph = transform<SwizzleScale>(graph, params, context.get());
graph = transform<AddPrefetch>(graph, params, context.get());
graph = transform<AddPRNG>(graph, context.get());
graph = transform<UpdateWavefrontParameters>(graph, params);
ControlFlowRWTracer tracer(graph);
const auto [a, b, c, d] = example.getOperationTags();
/** Yields accumulator macro tile tags encountered by following output edges */
auto accumulatorMacroTiles = [&](auto commandTag) -> Generator<int> {
for(auto tag : graph.coordinates.getNodes<User>())
{
const auto user = graph.coordinates.get<User>(tag).value();
if(user.commandTag == commandTag)
{
auto tags = graph.coordinates.followEdges<DataFlowEdge>({tag});
for(auto t : tags)
{
const auto node = graph.coordinates.getNode(t);
if(std::holds_alternative<MacroTile>(node))
{
const auto& macroTile = std::get<MacroTile>(node);
if(macroTile.layoutType == LayoutType::MATRIX_ACCUMULATOR)
{
co_yield t;
}
}
}
}
}
};
int kLoop = -1, kLoopTail = -1;
for(auto tag : graph.control.getNodes<ForLoopOp>())
{
const auto loop = graph.control.get<ForLoopOp>(tag).value();
if(loop.loopName == "KLoop")
kLoop = tag;
else if(loop.loopName == "KLoopTail")
kLoopTail = tag;
}
AssertFatal(kLoop != -1 && kLoopTail != -1, ShowValue(kLoop), ShowValue(kLoopTail));
SECTION("buildCoordinateLoopMapping")
{
auto loopMapping = kg::buildCoordinateLoopMapping(graph, tracer);
for(auto child : accumulatorMacroTiles(a))
{
for(const auto tag : graph.coordinates.getInputNodeIndices(child, isEdge<Duplicate>))
{
CAPTURE(child);
CHECK(loopMapping.at(tag).count(kLoop) > 0);
CHECK(loopMapping.at(tag).at(kLoop).size() == 16);
CHECK(loopMapping.at(tag).count(kLoopTail) > 0);
CHECK(loopMapping.at(tag).at(kLoopTail).size() == 8);
}
break; // second encountered macro tile and beyond are not used in kloop[tail]
}
}
SECTION("countCoordinateWritesInLoop")
{
{
bool didACheck = false;
for(auto tag : accumulatorMacroTiles(a))
{
CAPTURE(tag);
CHECK(countCoordinateWritesInLoop(graph, kLoop, tag, tracer) == 16);
for(const auto upstream :
graph.coordinates.getInputNodeIndices(tag, isEdge<Duplicate>))
{
CAPTURE(upstream);
CHECK(countCoordinateWritesInLoop(graph, kLoop, upstream, tracer) == 16);
CHECK(countCoordinateWritesInLoop(graph, kLoopTail, upstream, tracer) == 8);
didACheck = true;
}
break; // second encountered macro tile and beyond are not used in kloop[tail]
}
CHECK(didACheck);
}
{
bool didACheck = false;
for(const auto& c : graph.mapper.getConnections(kLoop))
{
CAPTURE(c.control);
if(std::holds_alternative<Connections::JustNaryArgument>(c.connection))
{
CAPTURE(c.coordinate);
// KLoop's for loop variable is only written in the KLoop, not in KLoopTail
CHECK(countCoordinateWritesInLoop(graph, kLoop, c.coordinate, tracer) == 1);
CHECK(countCoordinateWritesInLoop(graph, kLoopTail, c.coordinate, tracer) == 0);
didACheck = true;
}
}
CHECK(didACheck);
}
}
SECTION("valid hoist")
{
// Add assign node to kloop body that writes a constant to a new coordinate
// in the body of the kloop
// Verify that this node is hoisted to above the kloop
int coord = graph.coordinates.addElement(Linear{});
auto constantExpr = Expression::literal(42.0f);
Assign assignOp;
assignOp.expression = constantExpr;
int assign = graph.control.addElement(assignOp);
graph.mapper.connect(assign, coord, NaryArgument::DEST);
const auto firstDownstreamNode
= graph.control.getOutputNodeIndices<Body>(kLoop).take(1).only().value();
AssertFatal(firstDownstreamNode != -1, ShowValue(firstDownstreamNode));
insertBefore(graph, firstDownstreamNode, assign, assign);
CHECK(countCoordinateWritesInLoop(graph, kLoop, coord, ControlFlowRWTracer(graph)) == 1);
graph = transform<HoistLoopInvariant>(graph);
CHECK(countCoordinateWritesInLoop(graph, kLoop, coord, ControlFlowRWTracer(graph)) == 0);
int newAssign;
{
const auto connections = graph.mapper.getCoordinateConnections(coord);
REQUIRE(connections.size() == 1);
newAssign = connections[0].control;
}
{
const auto loopStack = controlStack(kLoop, graph);
const auto assignStack = controlStack(newAssign, graph);
REQUIRE(loopStack.size() == assignStack.size());
// Expect same control stack excluding itself
for(size_t i = 0; i < loopStack.size() - 1; ++i)
{
CHECK(loopStack[i] == assignStack[i]);
}
}
}
SECTION("no hoist incremented variable")
{
// Add assign node to kloop body that writes to incremented for loop variable
// Verify that this node is *not* hoisted
int forLoopCoordinate = -1;
for(const auto& c : graph.mapper.getConnections(kLoop))
{
CAPTURE(c.control);
if(std::holds_alternative<Connections::JustNaryArgument>(c.connection))
{
forLoopCoordinate = c.coordinate;
break;
}
}
AssertFatal(forLoopCoordinate != -1, "Could not find for loop coordinate");
Assign assignOp;
assignOp.expression = std::make_shared<Expression::Expression>(
Expression::DataFlowTag{forLoopCoordinate, Register::Type::Scalar, DataType::UInt32});
int assign = graph.control.addElement(assignOp);
graph.mapper.connect(assign, forLoopCoordinate, NaryArgument::DEST);
const auto firstDownstreamNode
= graph.control.getOutputNodeIndices<Body>(kLoop).take(1).only().value();
if(firstDownstreamNode != -1)
{
insertBefore(graph, firstDownstreamNode, assign, assign);
}
CHECK(
countCoordinateWritesInLoop(graph, kLoop, forLoopCoordinate, ControlFlowRWTracer(graph))
>= 1);
graph = transform<HoistLoopInvariant>(graph);
CHECK(
countCoordinateWritesInLoop(graph, kLoop, forLoopCoordinate, ControlFlowRWTracer(graph))
>= 1);
{
const auto loopStack = controlStack(kLoop, graph);
const auto assignStack = controlStack(assign, graph);
REQUIRE(loopStack.size() + 1 == assignStack.size());
// Assign stack has one more element which is itself
for(size_t i = 0; i < loopStack.size(); ++i)
{
CHECK(loopStack[i] == assignStack[i]);
}
CHECK(assignStack.back() == assign);
}
}
SECTION("no hoist when coordinate used after loop")
{
// Avoid hoisting when assigned-to coordinate is used after the loop,
// for the case where loop is zero-iteration
int coord = graph.coordinates.addElement(Linear{});
auto constantExpr = Expression::literal(42.0f);
Assign assignInLoop;
assignInLoop.expression = constantExpr;
int assignInLoopNode = graph.control.addElement(assignInLoop);
graph.mapper.connect(assignInLoopNode, coord, NaryArgument::DEST);
const auto firstDownstreamNode
= graph.control.getOutputNodeIndices<Body>(kLoop).take(1).only().value();
if(firstDownstreamNode != -1)
{
insertBefore(graph, firstDownstreamNode, assignInLoopNode, assignInLoopNode);
}
auto useExpr = std::make_shared<Expression::Expression>(
Expression::DataFlowTag{coord, Register::Type::Scalar, DataType::Float});
Assign assignAfterLoop;
assignAfterLoop.expression = useExpr;
int assignAfterLoopNode = graph.control.addElement(assignAfterLoop);
int resultCoord = graph.coordinates.addElement(Linear{});
graph.mapper.connect(assignAfterLoopNode, resultCoord, NaryArgument::DEST);
const auto nodeAfterLoop = graph.control.nodesAfter(kLoop).take(1).only().value();
AssertFatal(nodeAfterLoop != -1, ShowValue(nodeAfterLoop));
insertBefore(graph, nodeAfterLoop, assignAfterLoopNode, assignAfterLoopNode);
CHECK(countCoordinateWritesInLoop(graph, kLoop, coord, ControlFlowRWTracer(graph)) == 1);
graph = transform<HoistLoopInvariant>(graph);
CHECK(countCoordinateWritesInLoop(graph, kLoop, coord, ControlFlowRWTracer(graph)) == 1);
{
const auto loopStack = controlStack(kLoop, graph);
const auto assignStack = controlStack(assignInLoopNode, graph);
REQUIRE(loopStack.size() + 1 == assignStack.size());
for(size_t i = 0; i < loopStack.size(); ++i)
{
CHECK(loopStack[i] == assignStack[i]);
}
CHECK(assignStack.back() == assignInLoopNode);
}
}
SECTION("read-before-write prevents hoisting")
{
// Verify that when a coordinate is read before being written in a loop,
// the write cannot be hoisted (as it would change the first iteration's read value)
int coord = graph.coordinates.addElement(Linear{});
int tempCoord = graph.coordinates.addElement(Linear{});
Assign readNode;
readNode.expression = std::make_shared<Expression::Expression>(
Expression::DataFlowTag{coord, Register::Type::Scalar, DataType::Float});
int readNodeIdx = graph.control.addElement(readNode);
graph.mapper.connect(readNodeIdx, tempCoord, NaryArgument::DEST);
Assign writeNode;
writeNode.expression = Expression::literal(5.0f);
int writeNodeIdx = graph.control.addElement(writeNode);
graph.mapper.connect(writeNodeIdx, coord, NaryArgument::DEST);
const auto firstNode
= graph.control.getOutputNodeIndices<Body>(kLoop).take(1).only().value();
insertBefore(graph, firstNode, readNodeIdx, readNodeIdx);
insertAfter(graph, readNodeIdx, writeNodeIdx, writeNodeIdx);
CHECK(countCoordinateWritesInLoop(graph, kLoop, coord, ControlFlowRWTracer(graph)) == 1);
graph = transform<HoistLoopInvariant>(graph);
CHECK(countCoordinateWritesInLoop(graph, kLoop, coord, ControlFlowRWTracer(graph)) == 1);
const auto writeStack = controlStack(writeNodeIdx, graph);
CHECK(std::find(writeStack.begin(), writeStack.end(), kLoop) != writeStack.end());
}
SECTION("write-before-read allows hoisting")
{
// Verify that when a coordinate is written before being read in a loop,
// the write can be hoisted (as the read always sees the written value)
int coord = graph.coordinates.addElement(Linear{});
int tempCoord = graph.coordinates.addElement(Linear{});
Assign writeNode;
writeNode.expression = Expression::literal(5.0f);
int writeNodeIdx = graph.control.addElement(writeNode);
graph.mapper.connect(writeNodeIdx, coord, NaryArgument::DEST);
Assign readNode;
readNode.expression = std::make_shared<Expression::Expression>(
Expression::DataFlowTag{coord, Register::Type::Scalar, DataType::Float});
int readNodeIdx = graph.control.addElement(readNode);
graph.mapper.connect(readNodeIdx, tempCoord, NaryArgument::DEST);
const auto firstNode
= graph.control.getOutputNodeIndices<Body>(kLoop).take(1).only().value();
insertBefore(graph, firstNode, writeNodeIdx, writeNodeIdx);
insertAfter(graph, writeNodeIdx, readNodeIdx, readNodeIdx);
CHECK(countCoordinateWritesInLoop(graph, kLoop, coord, ControlFlowRWTracer(graph)) == 1);
graph = transform<HoistLoopInvariant>(graph);
CHECK(countCoordinateWritesInLoop(graph, kLoop, coord, ControlFlowRWTracer(graph)) == 0);
int newWriteNode = -1;
for(const auto& c : graph.mapper.getCoordinateConnections(coord))
{
if(graph.control.get<Assign>(c.control).has_value())
{
newWriteNode = c.control;
break;
}
}
REQUIRE(newWriteNode != -1);
const auto writeStack = controlStack(newWriteNode, graph);
const auto loopStack = controlStack(kLoop, graph);
CHECK(std::find(writeStack.begin(), writeStack.end(), kLoop) == writeStack.end());
REQUIRE(loopStack.size() == writeStack.size());
for(size_t i = 0; i < loopStack.size() - 1; ++i)
{
CHECK(loopStack[i] == writeStack[i]);
}
}
}