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

350 lines
13 KiB
C++

// Copyright Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
#include <catch2/catch_test_macros.hpp>
#include <rocRoller/CommonSubexpressionElim.hpp>
#include <rocRoller/Expression.hpp>
#include <rocRoller/ExpressionTransformations.hpp>
#include <rocRoller/KernelOptions.hpp>
#include <rocRoller/KernelOptions_detail.hpp>
#include "TestContext.hpp"
using namespace rocRoller;
namespace ExpressionTreeOrderingTest
{
/**
* Helper to verify that priorities form a valid ordering.
* Checks that all values are unique, in range [0, size), and dependencies are respected.
*/
bool isValidPriorityOrdering(Expression::ExpressionTree const& tree)
{
// All priority values should be unique and in range [0, tree.size())
std::set<int> seen;
for(auto const& node : tree)
{
if(node.priorityOrder < 0 || node.priorityOrder >= static_cast<int>(tree.size()))
return false;
if(seen.contains(node.priorityOrder))
return false;
seen.insert(node.priorityOrder);
}
if(seen.size() != tree.size())
return false;
// Verify dependencies are respected
for(size_t i = 0; i < tree.size(); ++i)
{
for(int dep : tree[i].deps)
{
if(tree[dep].priorityOrder >= tree[i].priorityOrder)
return false;
}
}
return true;
}
TEST_CASE("updatePriorityOrder produces valid ordering", "[expression][codegen]")
{
auto context = TestContext::ForDefaultTarget();
auto ra = std::make_shared<Register::Value>(
context.get(), Register::Type::Vector, DataType::Int32, 1);
ra->allocateNow();
auto rb = std::make_shared<Register::Value>(
context.get(), Register::Type::Vector, DataType::Int32, 1);
rb->allocateNow();
auto a = ra->expression();
auto b = rb->expression();
SECTION("Simple binary expression")
{
auto expr = a + b;
auto tree = Expression::consolidateSubExpressions(expr, context.get());
CHECK(isValidPriorityOrdering(tree));
}
SECTION("Nested expression 1")
{
auto expr = (a + b) * (a - b);
auto tree = Expression::consolidateSubExpressions(expr, context.get());
CHECK(isValidPriorityOrdering(tree));
}
SECTION("Nested expression 2")
{
// ((a + b) * (a + b)) - ((a - b) * (a - b))
auto sum = a + b;
auto diff = a - b;
auto expr = (sum * sum) - (diff * diff);
auto tree = Expression::consolidateSubExpressions(expr, context.get());
CHECK(isValidPriorityOrdering(tree));
}
SECTION("Multiple operations on same base with FastArithmetic")
{
auto dReg = std::make_shared<Register::Value>(
context.get(), Register::Type::Scalar, DataType::UInt32, 1);
dReg->allocateNow();
auto d = dReg->expression();
auto xReg = std::make_shared<Register::Value>(
context.get(), Register::Type::Scalar, DataType::UInt32, 1);
xReg->allocateNow();
auto x = xReg->expression();
// x / d, x % d, x * 2
auto quot = x / d;
auto rem = x % d;
auto mul = x * Expression::literal(2u);
auto expr = quot + rem + mul;
auto fast = Expression::FastArithmetic(context.get());
auto expanded = fast(expr);
auto tree = Expression::consolidateSubExpressions(expanded, context.get());
CHECK(isValidPriorityOrdering(tree));
}
}
TEST_CASE("Sethi-Ullman ordering prioritizes heavier subtrees", "[expression][codegen]")
{
auto context = TestContext::ForDefaultTarget();
auto ra = std::make_shared<Register::Value>(
context.get(), Register::Type::Vector, DataType::Int32, 1);
ra->allocateNow();
auto rb = std::make_shared<Register::Value>(
context.get(), Register::Type::Vector, DataType::Int32, 1);
rb->allocateNow();
auto rc = std::make_shared<Register::Value>(
context.get(), Register::Type::Vector, DataType::Int32, 1);
rc->allocateNow();
auto rd = std::make_shared<Register::Value>(
context.get(), Register::Type::Vector, DataType::Int32, 1);
rd->allocateNow();
auto re = std::make_shared<Register::Value>(
context.get(), Register::Type::Vector, DataType::Int32, 1);
re->allocateNow();
auto rf = std::make_shared<Register::Value>(
context.get(), Register::Type::Vector, DataType::Int32, 1);
rf->allocateNow();
auto a = ra->expression();
auto b = rb->expression();
auto c = rc->expression();
auto d = rd->expression();
auto e = re->expression();
auto f = rf->expression();
SECTION("Unbalanced tree - left-heavy with weight 3")
{
// Tree structure (unique operations):
//
// root(|)
// / \
// (*) (<<)
// / \ / \
// (+) (-) d neg(e)
// / \ / \ |
// a b c f e
//
// Sethi-Ullman weights:
//
// 3
// / \
// 3 2
// / \ / \
// 2 2 1 1
// / \ / \ |
// 1 1 1 1 1
//
auto inner1 = a + b; // Add
auto inner2 = c - f; // Subtract
auto heavy = inner1 * inner2; // Multiply
auto negE = -e; // Negate (unary)
auto lightSide = d << negE; // ShiftL
auto expr = heavy | lightSide; // BitwiseOr
auto tree = Expression::consolidateSubExpressions(expr, context.get());
CHECK(isValidPriorityOrdering(tree));
// Helper to find priority by operation type in the tree
auto priorityOf = [&](auto const& opType) -> int {
for(auto const& node : tree)
if(std::holds_alternative<std::decay_t<decltype(opType)>>(*node.expr))
return node.priorityOrder;
return -1;
};
// Get priorities by operation type
int pAdd = priorityOf(Expression::Add{});
int pSub = priorityOf(Expression::Subtract{});
int pMul = priorityOf(Expression::Multiply{});
int pNeg = priorityOf(Expression::Negate{});
int pShl = priorityOf(Expression::ShiftL{});
int pOr = priorityOf(Expression::BitwiseOr{});
// Dependency checks within heavy subtree
CHECK(pAdd < pMul); // Add before Multiply
CHECK(pSub < pMul); // Subtract before Multiply
// Dependency checks within light subtree
CHECK(pNeg < pShl); // Negate before ShiftL
// Heavy subtree (weight 3) must complete before light subtree (weight 2) starts
CHECK(pMul < pNeg); // Multiply before Negate
CHECK(pMul < pShl); // Multiply before ShiftL
// Root comes last
CHECK(pShl < pOr); // ShiftL before BitwiseOr
CHECK(pMul < pOr); // Multiply before BitwiseOr
// Within heavy subtree, one inner op completes before the other starts
bool addFirst = (pAdd < pSub);
bool subFirst = (pSub < pAdd);
CHECK((addFirst || subFirst));
}
}
TEST_CASE("maxConcurrentSubExpressions affects code generation order", "[expression][codegen]")
{
// Tree structure:
//
// root(+)
// / \
// (*) (*)
// / \ / \
// (+) (-) (<<) (>>)
// / \ / \ / \ / \
// a b c f a b c f
//
// priorityOrder is computed once by updatePriorityOrder and stays the same.
// maxConcurrentSubExpressions affects the scheduling order during code generation
// by limiting how many nodes can be "in-flight" at once (categoryLimit in generateNodes).
auto generateCode = [](int maxConcurrent) -> std::string {
KernelOptions opts;
opts->maxConcurrentSubExpressions = maxConcurrent;
auto context = TestContext::ForDefaultTarget(opts);
auto ra = std::make_shared<Register::Value>(
context.get(), Register::Type::Vector, DataType::Int32, 1);
ra->setName("ra");
ra->allocateNow();
auto rb = std::make_shared<Register::Value>(
context.get(), Register::Type::Vector, DataType::Int32, 1);
rb->setName("rb");
rb->allocateNow();
auto rc = std::make_shared<Register::Value>(
context.get(), Register::Type::Vector, DataType::Int32, 1);
rc->setName("rc");
rc->allocateNow();
auto rf = std::make_shared<Register::Value>(
context.get(), Register::Type::Vector, DataType::Int32, 1);
rf->setName("rf");
rf->allocateNow();
auto a = ra->expression();
auto b = rb->expression();
auto c = rc->expression();
auto f = rf->expression();
auto add = a + b;
auto sub = c - f;
auto shl = a << b;
auto shr = c >> f;
auto leftMul = add * sub;
auto rightMul = shl * shr;
auto root = leftMul + rightMul;
Register::ValuePtr dest;
context.get()->schedule(Expression::generate(dest, root, context.get()));
return context.output();
};
SECTION("maxConcurrentSubExpressions = 1 processes one subtree at a time")
{
auto code = generateCode(1);
// Find positions of key operations in generated code
auto addPos = code.find("v_add");
auto subPos = code.find("v_sub");
auto lshiftPos = code.find("v_lshl");
auto rshiftPos = code.find("v_ashr");
auto mulPos = code.find("v_mul");
// With max=1, one subtree should be fully completed (including its multiply)
// before the other subtree starts.
// Either: add, sub, mul all before lshift, rshift
// Or: lshift, rshift, mul all before add, sub
if(addPos != std::string::npos && subPos != std::string::npos
&& lshiftPos != std::string::npos && rshiftPos != std::string::npos
&& mulPos != std::string::npos)
{
bool leftFirst = (addPos < lshiftPos) && (addPos < rshiftPos)
&& (subPos < lshiftPos) && (subPos < rshiftPos);
bool rightFirst = (lshiftPos < addPos) && (lshiftPos < subPos)
&& (rshiftPos < addPos) && (rshiftPos < subPos);
CHECK((leftFirst || rightFirst));
// There should be a multiply before the second side starts
if(leftFirst)
{
// mul should come before lshift and rshift
CHECK(mulPos < lshiftPos);
CHECK(mulPos < rshiftPos);
}
else
{
// mul should come before add and sub
CHECK(mulPos < addPos);
CHECK(mulPos < subPos);
}
}
}
SECTION("maxConcurrentSubExpressions = 4 processes subtrees in parallel")
{
auto code = generateCode(4);
// Find positions of key operations in generated code
auto addPos = code.find("v_add");
auto subPos = code.find("v_sub");
auto lshiftPos = code.find("v_lshl");
auto rshiftPos = code.find("v_ashr");
auto mulPos = code.find("v_mul");
CHECK(!code.empty());
// With max=4, all leaf operations can be processed before any multiply.
// There should be no multiply in between the leaf operations.
if(addPos != std::string::npos && subPos != std::string::npos
&& lshiftPos != std::string::npos && rshiftPos != std::string::npos
&& mulPos != std::string::npos)
{
// Find the range of leaf operations
auto maxLeafPos = std::max({addPos, subPos, lshiftPos, rshiftPos});
// Multiply should come after all leaf operations
CHECK(mulPos > maxLeafPos);
}
}
}
}