Files
TheRock/rocm-libraries/shared/rocroller/test/catch/ExecuteMaskGeneratorTest.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

1101 lines
47 KiB
C++

// Copyright Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
#include <sstream>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include "CustomMatchers.hpp"
#include "CustomSections.hpp"
#include "TestContext.hpp"
#include <common/Utilities.hpp>
#include <rocRoller/AssemblyKernel.hpp>
#include <rocRoller/Expression.hpp>
#include <rocRoller/GPUArchitecture/GPUCapability.hpp>
#include <rocRoller/KernelArguments.hpp>
#include <rocRoller/KernelGraph/KernelGraph.hpp>
#include <hip/hip_runtime.h>
using namespace rocRoller;
using namespace rocRoller::KernelGraph;
using namespace rocRoller::KernelGraph::CoordinateGraph;
using namespace rocRoller::KernelGraph::ControlGraph;
using namespace Catch::Matchers;
namespace ExecuteMaskGeneratorTest
{
namespace kg = rocRoller::KernelGraph;
// Remove lines belonging to the workgroup-cluster prolog branch so that
// codegen assertions can reject unexpected s_cbranch_vcc without tripping
// on the legitimate s_cbranch_vccz emitted by the cluster detection in
// AssemblyKernel::prolog(). Returns the input unchanged when the target
// does not support workgroup clusters.
std::string StripClusterBranchLines(const std::string& asmOutput, ContextPtr ctx)
{
if(!ctx->targetArchitecture().HasCapability(GPUCapability::HasWorkgroupClusters))
return asmOutput;
std::istringstream stream(asmOutput);
std::string result;
std::string line;
while(std::getline(stream, line))
{
if(line.find("NotInCluster") == std::string::npos)
{
result += line;
result += '\n';
}
}
return result;
}
/**
* Builds a minimal KernelGraph for testing Exec and BranchAndExec ConditionalOp modes.
*
* Graph structure:
* Kernel
* +-- Body -> conditional (ConditionalOp with workitem ID odd/even condition)
* +-- Body -> trueOp (assigns 1u to destVGPR)
* +-- Else -> falseOp (assigns 2u to destVGPR) [if withElseBody]
*
* The condition ((workitemId & 1u) == 0u) checks whether the workitem ID is even.
* Comparing a VGPR expression to a scalar literal produces a VCC result, as required
* by Exec and BranchAndExec modes.
*/
kg::KernelGraph buildConditionalGraph(ConditionalMode mode,
bool withElseBody,
Register::ValuePtr workitemIdReg)
{
kg::KernelGraph kgraph;
auto zero = Expression::literal(0u);
auto one = Expression::literal(1u);
auto two = Expression::literal(2u);
// Workitem ID VGPR expression: (workitemId & 1u) == 0u is true for even lanes.
auto workitemId = workitemIdReg->expression();
auto isEven = (workitemId & one) == zero;
// Destination VGPR for body and else assigns.
auto destVGPR = kgraph.coordinates.addElement(VGPR());
auto initOp = kgraph.control.addElement(Assign{Register::Type::Vector, zero});
kgraph.mapper.connect(initOp, destVGPR, NaryArgument::DEST);
// True body: assign 1 to destVGPR.
auto trueOp = kgraph.control.addElement(Assign{Register::Type::Vector, one});
kgraph.mapper.connect(trueOp, destVGPR, NaryArgument::DEST);
// ConditionalOp: (workitemId & 1u) == 0u is a VGPR comparison whose result lands
// in VCC, satisfying the Exec/BranchAndExec requirement.
auto conditional
= kgraph.control.addElement(ConditionalOp{isEven, mode, "Exec Conditional"});
auto kernel = kgraph.control.addElement(Kernel());
kgraph.control.addElement(Body(), {kernel}, {initOp});
kgraph.control.addElement(Sequence(), {initOp}, {conditional});
kgraph.control.addElement(Body(), {conditional}, {trueOp});
if(withElseBody)
{
// False body: assign 2 to destVGPR.
auto falseOp = kgraph.control.addElement(Assign{Register::Type::Vector, two});
kgraph.mapper.connect(falseOp, destVGPR, NaryArgument::DEST);
kgraph.control.addElement(Else(), {conditional}, {falseOp});
}
return kgraph;
}
TEST_CASE("ExecuteMaskGenerator - Exec mode, true body only",
"[exec-mask][codegen][kernel-graph]")
{
SUPPORTED_ARCH_SECTION(arch)
{
auto testCtx = TestContext::ForTarget(arch);
auto ctx = testCtx.get();
auto k = ctx->kernel();
ctx->schedule(k->preamble());
ctx->schedule(k->prolog());
auto kgraph
= buildConditionalGraph(ConditionalMode::Exec, false, k->workitemIndex()[0]);
ctx->schedule(rocRoller::KernelGraph::generate(kgraph, k));
auto output = testCtx.output();
if(k->wavefront_size() == 64)
{
CHECK_THAT(output, ContainsSubstring("s_and_saveexec_b64"));
}
else
{
CHECK_THAT(output, ContainsSubstring("s_and_saveexec_b32"));
}
// No else body: complement mask instruction must not be emitted.
CHECK_THAT(output, !ContainsSubstring("s_andn1_saveexec"));
// Exec mode uses EXEC masking, not scalar SCC-based branches.
CHECK_THAT(output, !ContainsSubstring("s_cbranch_scc0"));
// Exec mode uses EXEC masking, not VCC-based branches.
CHECK_THAT(StripClusterBranchLines(output, ctx), !ContainsSubstring("s_cbranch_vcc"));
}
}
TEST_CASE("ExecuteMaskGenerator - Exec mode, true and else bodies",
"[exec-mask][codegen][kernel-graph]")
{
SUPPORTED_ARCH_SECTION(arch)
{
auto testCtx = TestContext::ForTarget(arch);
auto ctx = testCtx.get();
auto k = ctx->kernel();
ctx->schedule(k->preamble());
ctx->schedule(k->prolog());
auto kgraph = buildConditionalGraph(ConditionalMode::Exec, true, k->workitemIndex()[0]);
ctx->schedule(rocRoller::KernelGraph::generate(kgraph, k));
auto output = testCtx.output();
if(k->wavefront_size() == 64)
{
CHECK_THAT(output, ContainsSubstring("s_and_saveexec_b64"));
CHECK_THAT(output, ContainsSubstring("s_andn1_saveexec_b64"));
}
else
{
CHECK_THAT(output, ContainsSubstring("s_and_saveexec_b32"));
CHECK_THAT(output, ContainsSubstring("s_andn1_saveexec_b32"));
}
// Exec mode uses EXEC masking, not scalar SCC-based branches.
CHECK_THAT(output, !ContainsSubstring("s_cbranch_scc0"));
// Exec mode uses EXEC masking, not VCC-based branches.
CHECK_THAT(StripClusterBranchLines(output, ctx), !ContainsSubstring("s_cbranch_vcc"));
}
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec mode, true body only",
"[exec-mask][codegen][kernel-graph]")
{
SUPPORTED_ARCH_SECTION(arch)
{
auto testCtx = TestContext::ForTarget(arch);
auto ctx = testCtx.get();
auto k = ctx->kernel();
ctx->schedule(k->preamble());
ctx->schedule(k->prolog());
auto kgraph = buildConditionalGraph(
ConditionalMode::BranchAndExec, false, k->workitemIndex()[0]);
ctx->schedule(rocRoller::KernelGraph::generate(kgraph, k));
auto output = testCtx.output();
if(k->wavefront_size() == 64)
{
CHECK_THAT(output, ContainsSubstring("s_and_saveexec_b64"));
}
else
{
CHECK_THAT(output, ContainsSubstring("s_and_saveexec_b32"));
}
// BranchAndExec emits EXECZ-based branches and corresponding labels.
CHECK_THAT(output, ContainsSubstring("ELSE_Conditional_EXECZ_"));
CHECK_THAT(output, ContainsSubstring("EXIT_Conditional_EXECZ_"));
CHECK_THAT(output, ContainsSubstring("s_cbranch_execz"));
// Unconditional branch from the end of the true body to the exit label.
CHECK_THAT(output, ContainsSubstring("s_branch"));
// BranchAndExec does not use scalar SCC-based branches.
CHECK_THAT(output, !ContainsSubstring("s_cbranch_scc0"));
// BranchAndExec does not use VCC-based branches.
CHECK_THAT(StripClusterBranchLines(output, ctx), !ContainsSubstring("s_cbranch_vcc"));
}
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec mode, true and else bodies",
"[exec-mask][codegen][kernel-graph]")
{
SUPPORTED_ARCH_SECTION(arch)
{
auto testCtx = TestContext::ForTarget(arch);
auto ctx = testCtx.get();
auto k = ctx->kernel();
ctx->schedule(k->preamble());
ctx->schedule(k->prolog());
auto kgraph = buildConditionalGraph(
ConditionalMode::BranchAndExec, true, k->workitemIndex()[0]);
ctx->schedule(rocRoller::KernelGraph::generate(kgraph, k));
auto output = testCtx.output();
if(k->wavefront_size() == 64)
{
CHECK_THAT(output, ContainsSubstring("s_and_saveexec_b64"));
CHECK_THAT(output, ContainsSubstring("s_andn1_saveexec_b64"));
}
else
{
CHECK_THAT(output, ContainsSubstring("s_and_saveexec_b32"));
CHECK_THAT(output, ContainsSubstring("s_andn1_saveexec_b32"));
}
// BranchAndExec emits EXECZ-based branches and corresponding labels.
CHECK_THAT(output, ContainsSubstring("ELSE_Conditional_EXECZ_"));
CHECK_THAT(output, ContainsSubstring("EXIT_Conditional_EXECZ_"));
CHECK_THAT(output, ContainsSubstring("s_cbranch_execz"));
// Unconditional branch from the end of the true body to the exit label.
CHECK_THAT(output, ContainsSubstring("s_branch"));
// BranchAndExec does not use scalar SCC-based branches.
CHECK_THAT(output, !ContainsSubstring("s_cbranch_scc0"));
// BranchAndExec does not use VCC-based branches.
CHECK_THAT(StripClusterBranchLines(output, ctx), !ContainsSubstring("s_cbranch_vcc"));
}
}
/**
* Like buildConditionalGraph, but also adds a StoreVGPR that writes the per-lane
* result (1u for true lanes, 2u for false lanes) to a global output buffer at
* output[workitemId]. Requires a kernel argument named "output" (UInt32 pointer).
*
* Graph structure:
* Control graph:
* Kernel
* +-- Body -> initOp (assign 0u to destVGPR)
* +-- Sequence -> conditional (ConditionalOp: (workitemId & 1u) == 0u)
* +-- Body -> trueOp (assign 1u to destVGPR)
* +-- Else -> falseOp (assign 2u to destVGPR) [if withElseBody]
* +-- Sequence -> storeOp (StoreVGPR: destVGPR -> output[workitemId])
* Coordinate graph:
* Workitem(0) --PassThrough--> User("output") --PassThrough--> destVGPR (VGPR)
*/
kg::KernelGraph buildConditionalGraphWithStore(ConditionalMode mode,
bool withElseBody,
Register::ValuePtr workitemIdReg,
uint32_t wavefrontSize)
{
kg::KernelGraph kgraph;
auto zero = Expression::literal(0u);
auto one = Expression::literal(1u);
auto two = Expression::literal(2u);
auto workitemId = workitemIdReg->expression();
auto isEven = (workitemId & one) == zero;
// Destination VGPR for body and else assigns.
auto destVGPR = kgraph.coordinates.addElement(VGPR());
// Pre-initialize destVGPR to 0 so lanes that skip the true body have a
// known value.
auto initOp = kgraph.control.addElement(Assign{Register::Type::Vector, zero});
kgraph.mapper.connect(initOp, destVGPR, NaryArgument::DEST);
// True body: assign 1 to destVGPR.
auto trueOp = kgraph.control.addElement(Assign{Register::Type::Vector, one});
kgraph.mapper.connect(trueOp, destVGPR, NaryArgument::DEST);
auto conditional
= kgraph.control.addElement(ConditionalOp{isEven, mode, "Exec Conditional"});
auto kernel = kgraph.control.addElement(Kernel());
kgraph.control.addElement(Body(), {kernel}, {initOp});
kgraph.control.addElement(Sequence(), {initOp}, {conditional});
kgraph.control.addElement(Body(), {conditional}, {trueOp});
if(withElseBody)
{
// False body: assign 2 to destVGPR.
auto falseOp = kgraph.control.addElement(Assign{Register::Type::Vector, two});
kgraph.mapper.connect(falseOp, destVGPR, NaryArgument::DEST);
kgraph.control.addElement(Else(), {conditional}, {falseOp});
}
// Store each lane's result to output[workitemId].
auto wfSizeExpr = Expression::literal(wavefrontSize);
auto workitem0 = kgraph.coordinates.addElement(Workitem(0, wfSizeExpr));
auto user = kgraph.coordinates.addElement(User({}, "output"));
kgraph.coordinates.addElement(PassThrough(), {workitem0}, {user});
kgraph.coordinates.addElement(PassThrough(), {user}, {destVGPR});
auto storeOp = kgraph.control.addElement(StoreVGPR{});
kgraph.mapper.connect<User>(storeOp, user);
kgraph.mapper.connect<VGPR>(storeOp, destVGPR);
kgraph.control.addElement(Sequence(), {conditional}, {storeOp});
return kgraph;
}
// Helper used by the GPU execution tests below.
void runGPUExecutionTest(ConditionalMode mode, bool withElseBody)
{
auto testCtx = TestContext::ForTestDevice();
auto ctx = testCtx.get();
auto k = ctx->kernel();
auto wfSize = static_cast<uint32_t>(k->wavefront_size());
k->addArgument(
{"output", {DataType::UInt32, PointerType::PointerGlobal}, DataDirection::WriteOnly});
k->setKernelDimensions(1);
k->setWorkitemCount(
{Expression::literal(wfSize), Expression::literal(1u), Expression::literal(1u)});
k->setWorkgroupSize({wfSize, 1, 1});
ctx->schedule(k->preamble());
ctx->schedule(k->prolog());
auto kgraph
= buildConditionalGraphWithStore(mode, withElseBody, k->workitemIndex()[0], wfSize);
ctx->schedule(rocRoller::KernelGraph::generate(kgraph, k));
ctx->schedule(k->postamble());
ctx->schedule(k->amdgpu_metadata());
if(ctx->hipDeviceIndex() < 0)
SKIP("No HIP device present.");
auto deviceOutput = make_shared_device<uint32_t>(wfSize, 0u);
KernelArguments kargs(false);
kargs.append("output", deviceOutput.get());
KernelInvocation kinv;
kinv.workitemCount = {wfSize, 1, 1};
kinv.workgroupSize = {wfSize, 1, 1};
ctx->instructions()->getExecutableKernel()->executeKernel(kargs, kinv);
std::vector<uint32_t> hostOutput(wfSize);
REQUIRE_THAT(
hipMemcpy(
hostOutput.data(), deviceOutput.get(), wfSize * sizeof(uint32_t), hipMemcpyDefault),
HasHipSuccess(0));
// Even lanes (workitemId & 1 == 0) execute the true body -> 1.
// Odd lanes:
// Exec mode: else body runs per-lane -> 2, or skipped -> 0.
// BranchAndExec mode: else label only reached when EXEC==0 (no true lanes at all);
// with mixed lanes some true lanes exist, so else body never
// runs -> odd lanes retain their pre-initialized value of 0.
bool elseBodyRunsPerLane = withElseBody && (mode == ConditionalMode::Exec);
std::vector<uint32_t> expected(wfSize);
for(uint32_t i = 0; i < wfSize; ++i)
expected[i] = (i % 2 == 0) ? 1u : (elseBodyRunsPerLane ? 2u : 0u);
CHECK(hostOutput == expected);
}
TEST_CASE("ExecuteMaskGenerator - Exec mode, true body only (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTest(ConditionalMode::Exec, false);
}
TEST_CASE("ExecuteMaskGenerator - Exec mode, true and else bodies (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTest(ConditionalMode::Exec, true);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec mode, true body only (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTest(ConditionalMode::BranchAndExec, false);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec mode, true and else bodies (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTest(ConditionalMode::BranchAndExec, true);
}
/**
* Like buildConditionalGraphWithStore, but uses a condition that is false for every
* active lane so that s_and_saveexec_* produces EXEC==0 and the EXECZ branch is taken.
*
* Condition: workitemId >= wavefrontSize
* workitemId is always in [0, wavefrontSize), so this is always false for every lane,
* VCC==0, and EXECZ is set after s_and_saveexec_*.
*
* Graph structure:
* Kernel
* +-- Body -> initOp (assign 0u to destVGPR)
* +-- Sequence -> conditional (BranchAndExec: alwaysFalse)
* +-- Body -> trueOp (assign 1u) [skipped: EXECZ branch taken]
* +-- Else -> falseOp (assign 2u) [if withElseBody]
* +-- Sequence -> storeOp (StoreVGPR: destVGPR -> output[workitemId])
*
* Expected per-lane output:
* withElseBody==true: 2u for all lanes (else body runs with EXEC restored to full mask)
* withElseBody==false: 0u for all lanes (no body runs; pre-init value retained)
*/
kg::KernelGraph buildAlwaysFalseConditionalGraphWithStore(ConditionalMode mode,
bool withElseBody,
Register::ValuePtr workitemIdReg,
uint32_t wavefrontSize)
{
kg::KernelGraph kgraph;
auto zero = Expression::literal(0u);
auto one = Expression::literal(1u);
auto two = Expression::literal(2u);
// Always-false VCC condition: workitemId >= wavefrontSize.
// workitemId is always in [0, wfSize), so this is always false for every lane,
// and s_and_saveexec_* produces EXEC==0 (EXECZ set).
auto workitemId = workitemIdReg->expression();
auto alwaysFalse = workitemId >= Expression::literal(wavefrontSize);
auto destVGPR = kgraph.coordinates.addElement(VGPR());
auto initOp = kgraph.control.addElement(Assign{Register::Type::Vector, zero});
kgraph.mapper.connect(initOp, destVGPR, NaryArgument::DEST);
auto trueOp = kgraph.control.addElement(Assign{Register::Type::Vector, one});
kgraph.mapper.connect(trueOp, destVGPR, NaryArgument::DEST);
auto conditional = kgraph.control.addElement(
ConditionalOp{alwaysFalse, mode, "AlwaysFalse Conditional"});
auto kernel = kgraph.control.addElement(Kernel());
kgraph.control.addElement(Body(), {kernel}, {initOp});
kgraph.control.addElement(Sequence(), {initOp}, {conditional});
kgraph.control.addElement(Body(), {conditional}, {trueOp});
if(withElseBody)
{
auto falseOp = kgraph.control.addElement(Assign{Register::Type::Vector, two});
kgraph.mapper.connect(falseOp, destVGPR, NaryArgument::DEST);
kgraph.control.addElement(Else(), {conditional}, {falseOp});
}
auto wfSizeExpr = Expression::literal(wavefrontSize);
auto workitem0 = kgraph.coordinates.addElement(Workitem(0, wfSizeExpr));
auto user = kgraph.coordinates.addElement(User({}, "output"));
kgraph.coordinates.addElement(PassThrough(), {workitem0}, {user});
kgraph.coordinates.addElement(PassThrough(), {user}, {destVGPR});
auto storeOp = kgraph.control.addElement(StoreVGPR{});
kgraph.mapper.connect<User>(storeOp, user);
kgraph.mapper.connect<VGPR>(storeOp, destVGPR);
kgraph.control.addElement(Sequence(), {conditional}, {storeOp});
return kgraph;
}
// Helper for EXECZ-forced GPU execution tests.
// The condition is always false for every lane so s_and_saveexec_* produces
// EXEC==0, EXECZ is set, and the EXECZ branch is taken at runtime.
void runGPUExecutionTestEXECZ(bool withElseBody)
{
auto testCtx = TestContext::ForTestDevice();
auto ctx = testCtx.get();
auto k = ctx->kernel();
auto wfSize = static_cast<uint32_t>(k->wavefront_size());
k->addArgument(
{"output", {DataType::UInt32, PointerType::PointerGlobal}, DataDirection::WriteOnly});
k->setKernelDimensions(1);
k->setWorkitemCount(
{Expression::literal(wfSize), Expression::literal(1u), Expression::literal(1u)});
k->setWorkgroupSize({wfSize, 1, 1});
ctx->schedule(k->preamble());
ctx->schedule(k->prolog());
auto kgraph = buildAlwaysFalseConditionalGraphWithStore(
ConditionalMode::BranchAndExec, withElseBody, k->workitemIndex()[0], wfSize);
ctx->schedule(rocRoller::KernelGraph::generate(kgraph, k));
ctx->schedule(k->postamble());
ctx->schedule(k->amdgpu_metadata());
if(ctx->hipDeviceIndex() < 0)
SKIP("No HIP device present.");
auto deviceOutput = make_shared_device<uint32_t>(wfSize, 0u);
KernelArguments kargs(false);
kargs.append("output", deviceOutput.get());
KernelInvocation kinv;
kinv.workitemCount = {wfSize, 1, 1};
kinv.workgroupSize = {wfSize, 1, 1};
ctx->instructions()->getExecutableKernel()->executeKernel(kargs, kinv);
std::vector<uint32_t> hostOutput(wfSize);
REQUIRE_THAT(
hipMemcpy(
hostOutput.data(), deviceOutput.get(), wfSize * sizeof(uint32_t), hipMemcpyDefault),
HasHipSuccess(0));
// The condition (workitemId >= wavefrontSize) is false for every lane.
// s_and_saveexec_* produces EXEC==0 so EXECZ is set and the s_cbranch_execz
// over the true body is taken; the true body is never executed.
//
// With else body: EXEC is restored to the original mask, s_andn1_saveexec_*
// computes EXEC AND NOT(vcc) = originalEXEC AND ~0 = originalEXEC, leaving
// all lanes active for the else body (assign 2u).
// Without else body: destVGPR retains its pre-initialized value (0u) for all lanes,
// and EXEC is correctly restored to the original mask at the exit label.
uint32_t expectedValue = withElseBody ? 2u : 0u;
std::vector<uint32_t> expected(wfSize, expectedValue);
CHECK(hostOutput == expected);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec mode, EXECZ forced (all-lanes false, true body "
"only) (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestEXECZ(false);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec mode, EXECZ forced (all-lanes false, with else "
"body) (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestEXECZ(true);
}
/**
* Like buildConditionalGraphWithStore, but adds a nested ConditionalOp inside the
* true body that overwrites destVGPR with 4u for lanes whose workitem ID is
* divisible by 4 (workitemId & 3u == 0u), and optionally 8u for the inner false
* lanes (even but not div-by-4).
*
* True-body execution sequence for a given lane:
* 1. Assign 1u to destVGPR (outer true body).
* 2. If (workitemId & 3u) == 0u, assign 4u to destVGPR (inner true);
* else if withInnerElseBody, assign 8u to destVGPR (inner else).
*
* Expected per-lane result:
* workitemId % 4 == 0 -> 4u
* workitemId % 2 == 0 -> 8u if (innerMode==Exec && withInnerElseBody), else 1u
* odd workitemId -> 2u if (outerMode==Exec && withOuterElseBody), else 0u
*
* outerMode and innerMode may differ to test mixed-mode nesting.
*
* Graph structure:
* Control graph:
* Kernel
* +-- Body -> initOp (assign 0u to destVGPR)
* +-- Sequence -> conditional (outer ConditionalOp: (workitemId & 1u) == 0u)
* +-- Body -> trueOp (assign 1u to destVGPR)
* -> innerConditional (ConditionalOp: (workitemId & 3u) == 0u)
* +-- Body -> innerTrueOp (assign 4u to destVGPR)
* +-- Else -> innerFalseOp (assign 8u to destVGPR) [if withInnerElseBody]
* +-- Else -> falseOp (assign 2u to destVGPR) [if withOuterElseBody]
* +-- Sequence -> storeOp (StoreVGPR: destVGPR -> output[workitemId])
* Coordinate graph:
* Workitem(0) --PassThrough--> User("output") --PassThrough--> destVGPR (VGPR)
*/
kg::KernelGraph buildNestedConditionalGraphWithStore(ConditionalMode outerMode,
ConditionalMode innerMode,
bool withOuterElseBody,
bool withInnerElseBody,
Register::ValuePtr workitemIdReg,
uint32_t wavefrontSize)
{
kg::KernelGraph kgraph;
auto zero = Expression::literal(0u);
auto one = Expression::literal(1u);
auto two = Expression::literal(2u);
auto three = Expression::literal(3u);
auto four = Expression::literal(4u);
auto eight = Expression::literal(8u);
auto workitemId = workitemIdReg->expression();
auto isEven = (workitemId & one) == zero;
auto isDivBy4 = (workitemId & three) == zero;
// Destination VGPR written by all assign ops.
auto destVGPR = kgraph.coordinates.addElement(VGPR());
// Pre-initialize destVGPR to 0.
auto initOp = kgraph.control.addElement(Assign{Register::Type::Vector, zero});
kgraph.mapper.connect(initOp, destVGPR, NaryArgument::DEST);
// Outer true body: assign 1 to destVGPR.
auto trueOp = kgraph.control.addElement(Assign{Register::Type::Vector, one});
kgraph.mapper.connect(trueOp, destVGPR, NaryArgument::DEST);
// Inner conditional (nested inside outer true body): assign 4 for div-by-4 lanes.
auto innerTrueOp = kgraph.control.addElement(Assign{Register::Type::Vector, four});
kgraph.mapper.connect(innerTrueOp, destVGPR, NaryArgument::DEST);
auto innerConditional
= kgraph.control.addElement(ConditionalOp{isDivBy4, innerMode, "DivBy4 Conditional"});
kgraph.control.addElement(Body(), {innerConditional}, {innerTrueOp});
if(withInnerElseBody)
{
// Inner false body: assign 8 to destVGPR (even but not div-by-4).
auto innerFalseOp = kgraph.control.addElement(Assign{Register::Type::Vector, eight});
kgraph.mapper.connect(innerFalseOp, destVGPR, NaryArgument::DEST);
kgraph.control.addElement(Else(), {innerConditional}, {innerFalseOp});
}
auto conditional
= kgraph.control.addElement(ConditionalOp{isEven, outerMode, "Exec Conditional"});
auto kernel = kgraph.control.addElement(Kernel());
kgraph.control.addElement(Body(), {kernel}, {initOp});
kgraph.control.addElement(Sequence(), {initOp}, {conditional});
// True body: trueOp then innerConditional.
kgraph.control.addElement(Body(), {conditional}, {trueOp});
kgraph.control.addElement(Sequence(), {trueOp}, {innerConditional});
if(withOuterElseBody)
{
// False body: assign 2 to destVGPR.
auto falseOp = kgraph.control.addElement(Assign{Register::Type::Vector, two});
kgraph.mapper.connect(falseOp, destVGPR, NaryArgument::DEST);
kgraph.control.addElement(Else(), {conditional}, {falseOp});
}
// Store each lane's result to output[workitemId].
auto wfSizeExpr = Expression::literal(wavefrontSize);
auto workitem0 = kgraph.coordinates.addElement(Workitem(0, wfSizeExpr));
auto user = kgraph.coordinates.addElement(User({}, "output"));
kgraph.coordinates.addElement(PassThrough(), {workitem0}, {user});
kgraph.coordinates.addElement(PassThrough(), {user}, {destVGPR});
auto storeOp = kgraph.control.addElement(StoreVGPR{});
kgraph.mapper.connect<User>(storeOp, user);
kgraph.mapper.connect<VGPR>(storeOp, destVGPR);
kgraph.control.addElement(Sequence(), {conditional}, {storeOp});
return kgraph;
}
// Helper used by the nested-conditional GPU execution tests below.
// outerMode and innerMode may differ to exercise mixed-mode nesting.
void runGPUExecutionTestNested(ConditionalMode outerMode,
ConditionalMode innerMode,
bool withOuterElseBody,
bool withInnerElseBody)
{
auto testCtx = TestContext::ForTestDevice();
auto ctx = testCtx.get();
auto k = ctx->kernel();
auto wfSize = static_cast<uint32_t>(k->wavefront_size());
k->addArgument(
{"output", {DataType::UInt32, PointerType::PointerGlobal}, DataDirection::WriteOnly});
k->setKernelDimensions(1);
k->setWorkitemCount(
{Expression::literal(wfSize), Expression::literal(1u), Expression::literal(1u)});
k->setWorkgroupSize({wfSize, 1, 1});
ctx->schedule(k->preamble());
ctx->schedule(k->prolog());
auto kgraph = buildNestedConditionalGraphWithStore(outerMode,
innerMode,
withOuterElseBody,
withInnerElseBody,
k->workitemIndex()[0],
wfSize);
ctx->schedule(rocRoller::KernelGraph::generate(kgraph, k));
ctx->schedule(k->postamble());
ctx->schedule(k->amdgpu_metadata());
if(ctx->hipDeviceIndex() < 0)
SKIP("No HIP device present.");
auto deviceOutput = make_shared_device<uint32_t>(wfSize, 0u);
KernelArguments kargs(false);
kargs.append("output", deviceOutput.get());
KernelInvocation kinv;
kinv.workitemCount = {wfSize, 1, 1};
kinv.workgroupSize = {wfSize, 1, 1};
ctx->instructions()->getExecutableKernel()->executeKernel(kargs, kinv);
std::vector<uint32_t> hostOutput(wfSize);
REQUIRE_THAT(
hipMemcpy(
hostOutput.data(), deviceOutput.get(), wfSize * sizeof(uint32_t), hipMemcpyDefault),
HasHipSuccess(0));
// Exec mode else body runs per-lane; BranchAndExec else body only runs when the
// entire EXEC mask is zero (all active lanes failed the condition), which never
// happens for the inner condition across a mixed warp, so it effectively never runs.
bool outerElseRunsPerLane = withOuterElseBody && (outerMode == ConditionalMode::Exec);
bool innerElseRunsPerLane = withInnerElseBody && (innerMode == ConditionalMode::Exec);
std::vector<uint32_t> expected(wfSize);
for(uint32_t i = 0; i < wfSize; ++i)
{
if(i % 4 == 0)
expected[i] = 4u; // outer true + inner true
else if(i % 2 == 0)
expected[i] = innerElseRunsPerLane ? 8u : 1u; // outer true + inner false/skipped
else
expected[i] = outerElseRunsPerLane ? 2u : 0u; // outer false/skipped
}
CHECK(hostOutput == expected);
}
TEST_CASE("ExecuteMaskGenerator - Exec mode, nested conditional (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(ConditionalMode::Exec, ConditionalMode::Exec, false, false);
}
TEST_CASE(
"ExecuteMaskGenerator - Exec mode, nested conditional with outer else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(ConditionalMode::Exec, ConditionalMode::Exec, true, false);
}
TEST_CASE(
"ExecuteMaskGenerator - Exec mode, nested conditional with inner else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(ConditionalMode::Exec, ConditionalMode::Exec, false, true);
}
TEST_CASE("ExecuteMaskGenerator - Exec mode, nested conditional with outer and inner else (GPU "
"execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(ConditionalMode::Exec, ConditionalMode::Exec, true, true);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec mode, nested conditional (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(
ConditionalMode::BranchAndExec, ConditionalMode::BranchAndExec, false, false);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec mode, nested conditional with outer else (GPU "
"execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(
ConditionalMode::BranchAndExec, ConditionalMode::BranchAndExec, true, false);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec mode, nested conditional with inner else (GPU "
"execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(
ConditionalMode::BranchAndExec, ConditionalMode::BranchAndExec, false, true);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec mode, nested conditional with outer and inner "
"else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(
ConditionalMode::BranchAndExec, ConditionalMode::BranchAndExec, true, true);
}
// Mixed-mode nesting: outer Exec, inner BranchAndExec.
TEST_CASE("ExecuteMaskGenerator - Exec outer / BranchAndExec inner, nested conditional (GPU "
"execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(
ConditionalMode::Exec, ConditionalMode::BranchAndExec, false, false);
}
TEST_CASE("ExecuteMaskGenerator - Exec outer / BranchAndExec inner, nested conditional with "
"outer else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(
ConditionalMode::Exec, ConditionalMode::BranchAndExec, true, false);
}
TEST_CASE("ExecuteMaskGenerator - Exec outer / BranchAndExec inner, nested conditional with "
"inner else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(
ConditionalMode::Exec, ConditionalMode::BranchAndExec, false, true);
}
TEST_CASE("ExecuteMaskGenerator - Exec outer / BranchAndExec inner, nested conditional with "
"outer and inner else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(
ConditionalMode::Exec, ConditionalMode::BranchAndExec, true, true);
}
// Mixed-mode nesting: outer BranchAndExec, inner Exec.
TEST_CASE("ExecuteMaskGenerator - BranchAndExec outer / Exec inner, nested conditional (GPU "
"execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(
ConditionalMode::BranchAndExec, ConditionalMode::Exec, false, false);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec outer / Exec inner, nested conditional with "
"outer else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(
ConditionalMode::BranchAndExec, ConditionalMode::Exec, true, false);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec outer / Exec inner, nested conditional with "
"inner else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(
ConditionalMode::BranchAndExec, ConditionalMode::Exec, false, true);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec outer / Exec inner, nested conditional with "
"outer and inner else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNested(
ConditionalMode::BranchAndExec, ConditionalMode::Exec, true, true);
}
/**
* Builds a graph with a nested ConditionalOp inside the ELSE body of the outer conditional.
*
* Graph structure:
* Kernel
* +-- Body -> initOp (assign 0u)
* +-- Sequence -> outer conditional (isEven)
* +-- Body -> trueOp (assign 1u)
* +-- Else -> outerFalseOp (assign 2u)
* -> innerConditional (isLowOdd = i%4==1)
* +-- Body -> innerTrueOp (assign 5u)
* +-- Else -> innerFalseOp (assign 3u) [if withInnerElseBody]
*
* Expected per-lane result (outerMode==Exec):
* even -> 1u (outer true)
* i % 4 == 1 -> 5u (outer else + inner true: bit 1 clear among odd lanes)
* i % 4 == 3 -> 3u if (innerMode==Exec && withInnerElseBody), else 2u
*
* For outerMode==BranchAndExec the outer else body never runs (EXECZ is never set
* with a mixed warp), so all odd lanes stay at 0u.
*/
kg::KernelGraph buildNestedInElseConditionalGraphWithStore(ConditionalMode outerMode,
ConditionalMode innerMode,
bool withInnerElseBody,
Register::ValuePtr workitemIdReg,
uint32_t wavefrontSize)
{
kg::KernelGraph kgraph;
auto zero = Expression::literal(0u);
auto one = Expression::literal(1u);
auto two = Expression::literal(2u);
auto three = Expression::literal(3u);
auto five = Expression::literal(5u);
auto workitemId = workitemIdReg->expression();
auto isEven = (workitemId & one) == zero;
// Among odd lanes, bit 1 is clear for lanes 1,5,9,... and set for 3,7,11,...
// This gives a non-trivial partition of the active (odd) lanes in the else body.
auto isBit1Clear = (workitemId & two) == zero;
auto destVGPR = kgraph.coordinates.addElement(VGPR());
auto initOp = kgraph.control.addElement(Assign{Register::Type::Vector, zero});
kgraph.mapper.connect(initOp, destVGPR, NaryArgument::DEST);
auto trueOp = kgraph.control.addElement(Assign{Register::Type::Vector, one});
kgraph.mapper.connect(trueOp, destVGPR, NaryArgument::DEST);
// Outer else body: assign 2, then run inner conditional.
auto outerFalseOp = kgraph.control.addElement(Assign{Register::Type::Vector, two});
kgraph.mapper.connect(outerFalseOp, destVGPR, NaryArgument::DEST);
auto innerTrueOp = kgraph.control.addElement(Assign{Register::Type::Vector, five});
kgraph.mapper.connect(innerTrueOp, destVGPR, NaryArgument::DEST);
auto innerConditional = kgraph.control.addElement(
ConditionalOp{isBit1Clear, innerMode, "Bit1Clear Conditional"});
kgraph.control.addElement(Body(), {innerConditional}, {innerTrueOp});
if(withInnerElseBody)
{
// Inner false body: assign 3 to destVGPR (odd lanes where bit 1 is set, i%4==3).
auto innerFalseOp = kgraph.control.addElement(Assign{Register::Type::Vector, three});
kgraph.mapper.connect(innerFalseOp, destVGPR, NaryArgument::DEST);
kgraph.control.addElement(Else(), {innerConditional}, {innerFalseOp});
}
auto conditional
= kgraph.control.addElement(ConditionalOp{isEven, outerMode, "Exec Conditional"});
auto kernel = kgraph.control.addElement(Kernel());
kgraph.control.addElement(Body(), {kernel}, {initOp});
kgraph.control.addElement(Sequence(), {initOp}, {conditional});
kgraph.control.addElement(Body(), {conditional}, {trueOp});
// Else body: outerFalseOp then innerConditional.
kgraph.control.addElement(Else(), {conditional}, {outerFalseOp});
kgraph.control.addElement(Sequence(), {outerFalseOp}, {innerConditional});
auto wfSizeExpr = Expression::literal(wavefrontSize);
auto workitem0 = kgraph.coordinates.addElement(Workitem(0, wfSizeExpr));
auto user = kgraph.coordinates.addElement(User({}, "output"));
kgraph.coordinates.addElement(PassThrough(), {workitem0}, {user});
kgraph.coordinates.addElement(PassThrough(), {user}, {destVGPR});
auto storeOp = kgraph.control.addElement(StoreVGPR{});
kgraph.mapper.connect<User>(storeOp, user);
kgraph.mapper.connect<VGPR>(storeOp, destVGPR);
kgraph.control.addElement(Sequence(), {conditional}, {storeOp});
return kgraph;
}
void runGPUExecutionTestNestedInElse(ConditionalMode outerMode,
ConditionalMode innerMode,
bool withInnerElseBody)
{
auto testCtx = TestContext::ForTestDevice();
auto ctx = testCtx.get();
auto k = ctx->kernel();
auto wfSize = static_cast<uint32_t>(k->wavefront_size());
k->addArgument(
{"output", {DataType::UInt32, PointerType::PointerGlobal}, DataDirection::WriteOnly});
k->setKernelDimensions(1);
k->setWorkitemCount(
{Expression::literal(wfSize), Expression::literal(1u), Expression::literal(1u)});
k->setWorkgroupSize({wfSize, 1, 1});
ctx->schedule(k->preamble());
ctx->schedule(k->prolog());
auto kgraph = buildNestedInElseConditionalGraphWithStore(
outerMode, innerMode, withInnerElseBody, k->workitemIndex()[0], wfSize);
ctx->schedule(rocRoller::KernelGraph::generate(kgraph, k));
ctx->schedule(k->postamble());
ctx->schedule(k->amdgpu_metadata());
if(ctx->hipDeviceIndex() < 0)
SKIP("No HIP device present.");
auto deviceOutput = make_shared_device<uint32_t>(wfSize, 0u);
KernelArguments kargs(false);
kargs.append("output", deviceOutput.get());
KernelInvocation kinv;
kinv.workitemCount = {wfSize, 1, 1};
kinv.workgroupSize = {wfSize, 1, 1};
ctx->instructions()->getExecutableKernel()->executeKernel(kargs, kinv);
std::vector<uint32_t> hostOutput(wfSize);
REQUIRE_THAT(
hipMemcpy(
hostOutput.data(), deviceOutput.get(), wfSize * sizeof(uint32_t), hipMemcpyDefault),
HasHipSuccess(0));
// For outerMode==Exec the else body runs per-lane for odd lanes.
// isBit1Clear is true when bit 1 of workitemId is 0, i.e. i%4==1 (lanes 1,5,9,...).
// For outerMode==BranchAndExec the else body only runs when all active lanes
// fail isEven, which never happens with a mixed warp, so odd lanes stay at 0u.
// The inner else (assign 3u) runs per-lane only when innerMode==Exec && withInnerElseBody.
bool innerElseRunsPerLane = withInnerElseBody && (innerMode == ConditionalMode::Exec);
std::vector<uint32_t> expected(wfSize);
for(uint32_t i = 0; i < wfSize; ++i)
{
if(i % 2 == 0)
expected[i] = 1u; // outer true
else if(outerMode == ConditionalMode::Exec)
expected[i] = (i % 4 == 1) ? 5u : (innerElseRunsPerLane ? 3u : 2u);
else
expected[i] = 0u; // BranchAndExec outer else never runs per-lane
}
CHECK(hostOutput == expected);
}
TEST_CASE("ExecuteMaskGenerator - Exec outer / Exec inner, nested conditional in else (GPU "
"execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNestedInElse(ConditionalMode::Exec, ConditionalMode::Exec, false);
}
TEST_CASE("ExecuteMaskGenerator - Exec outer / BranchAndExec inner, nested conditional in else "
"(GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNestedInElse(
ConditionalMode::Exec, ConditionalMode::BranchAndExec, false);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec outer / Exec inner, nested conditional in else "
"(GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNestedInElse(
ConditionalMode::BranchAndExec, ConditionalMode::Exec, false);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec outer / BranchAndExec inner, nested "
"conditional in else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNestedInElse(
ConditionalMode::BranchAndExec, ConditionalMode::BranchAndExec, false);
}
TEST_CASE("ExecuteMaskGenerator - Exec outer / Exec inner, nested conditional with inner else "
"in else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNestedInElse(ConditionalMode::Exec, ConditionalMode::Exec, true);
}
TEST_CASE("ExecuteMaskGenerator - Exec outer / BranchAndExec inner, nested conditional with "
"inner else in else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNestedInElse(
ConditionalMode::Exec, ConditionalMode::BranchAndExec, true);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec outer / Exec inner, nested conditional with "
"inner else in else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNestedInElse(
ConditionalMode::BranchAndExec, ConditionalMode::Exec, true);
}
TEST_CASE("ExecuteMaskGenerator - BranchAndExec outer / BranchAndExec inner, nested "
"conditional with inner else in else (GPU execution)",
"[exec-mask][gpu]")
{
runGPUExecutionTestNestedInElse(
ConditionalMode::BranchAndExec, ConditionalMode::BranchAndExec, true);
}
} // namespace ExecuteMaskGeneratorTest