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

378 lines
16 KiB
C++

// Copyright (c) 2026 Advanced Micro Devices, Inc.
// SPDX-License-Identifier: MIT
#include "rocjitsu/analysis/exec_state.h"
#include "rocjitsu/analysis/liveness.h"
#include "rocjitsu/code/basic_block.h"
#include "rocjitsu/code/code_object.h"
#include "rocjitsu/code/dbt/semantic/cdna3_scratch.h"
#include "rocjitsu/code/dbt/semantic_scratch.h"
#include "rocjitsu/code/dbt/translation_rule.h"
#include "rocjitsu/code/rj_code.h"
#include "rocjitsu/isa/arch/amdgpu/cdna3/builders.h"
#include "rocjitsu/isa/arch/amdgpu/cdna3/machine_insts.h"
#include "rocjitsu/isa/arch/amdgpu/cdna3/opcodes.h"
#include "rocjitsu/isa/arch/amdgpu/cdna4/builders.h"
#include "rocjitsu/isa/arch/amdgpu/cdna4/opcodes.h"
#include "rocjitsu/isa/decoder.h"
#include "rocjitsu/isa/instruction.h"
#include <gtest/gtest.h>
#include <array>
#include <cstdint>
#include <cstring>
#include <iterator>
#include <memory>
#include <span>
#include <vector>
namespace rocjitsu {
namespace {
// TODO: Consolidate this in-memory code-object fixture with the equivalent
// liveness and spill-manager test scaffolding.
class ScratchTestTextSection : public Section {
public:
ScratchTestTextSection(std::unique_ptr<char[]> data, std::size_t size)
: Section(".text", std::move(data)), size_(size) {}
std::size_t size() const override { return size_; }
uint32_t sectionHeaderNameIdx() const override { return 0; }
uint64_t sectionOffset() const override { return 0; }
private:
std::size_t size_;
};
class ScratchTestCodeObject : public CodeObject {
public:
explicit ScratchTestCodeObject(std::span<const uint32_t> words) {
const auto byte_size = words.size_bytes();
image_.resize(byte_size);
std::memcpy(image_.data(), words.data(), byte_size);
auto data = std::make_unique<char[]>(byte_size);
std::memcpy(data.get(), words.data(), byte_size);
sections_.push_back(std::make_unique<ScratchTestTextSection>(std::move(data), byte_size));
text_sections_.push_back(sections_.back().get());
}
};
std::vector<std::unique_ptr<BasicBlock>> build_scratch_test_blocks() {
constexpr auto move = cdna3::build_vop1(cdna3::kVMovB32Vop1, {.src0 = 256, .vdst = 0});
constexpr auto end = cdna3::build_sopp(cdna3::kSEndpgmSopp);
constexpr std::array words = {move[0], end[0]};
ScratchTestCodeObject code(words);
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA3);
return BasicBlock::build(code, *decoder, ROCJITSU_CODE_ARCH_CDNA3);
}
std::vector<BasicBlock *>
scratch_test_scope(const std::vector<std::unique_ptr<BasicBlock>> &blocks) {
std::vector<BasicBlock *> scope;
scope.reserve(blocks.size());
for (const auto &block : blocks)
scope.push_back(block.get());
return scope;
}
std::span<const uint8_t> scratch_test_text(const CodeObject &code) {
const Section *text = code.text_sections().front();
return {reinterpret_cast<const uint8_t *>(text->data()), text->size()};
}
TEST(SemanticSpillFrame, SeparatesPersistentAndTransientRanges) {
TranslationContext context(/*vgprs=*/8, /*agprs=*/0, /*accum_base=*/0,
/*sgprs=*/8, /*private_bytes=*/20);
const auto persistent = context.reserve_persistent_semantic_spill_dwords(2);
ASSERT_TRUE(persistent);
ASSERT_EQ(*persistent, 32u);
SemanticSpillFrame frame(context);
const auto first = frame.allocate_dwords(3, /*byte_alignment=*/4);
const auto second = frame.allocate_dwords(2, /*byte_alignment=*/8);
ASSERT_TRUE(first);
ASSERT_TRUE(second);
// Persistent storage ends at byte 40 and the transient frame is aligned to
// byte 48. The second allocation follows the first rather than reusing it.
EXPECT_EQ(first->byte_offset, 48u);
EXPECT_EQ(second->byte_offset, 64u);
EXPECT_EQ(context.required_private_segment_fixed_size, 72u);
}
TEST(SemanticSpillFrame, PersistentReservationRejects32BitOverflow) {
// A guest kernel can advertise a private size near UINT32_MAX. Aligning that up
// and extending it for a spill must fail closed rather than wrap to a low
// offset that would corrupt guest scratch.
TranslationContext context(/*vgprs=*/8, /*agprs=*/0, /*accum_base=*/0,
/*sgprs=*/8, /*private_bytes=*/0xfffffff8u);
const auto persistent = context.reserve_persistent_semantic_spill_dwords(2);
EXPECT_FALSE(persistent);
}
TEST(SemanticSpillFrame, TransientReservationRejects32BitOverflow) {
TranslationContext context(/*vgprs=*/8, /*agprs=*/0, /*accum_base=*/0,
/*sgprs=*/8, /*private_bytes=*/0xfffffff8u);
const auto transient = context.reserve_semantic_spill_dwords(2);
EXPECT_FALSE(transient);
}
TEST(SemanticSpillFrame, NewInstructionsReuseTransientFrameBase) {
TranslationContext context(/*vgprs=*/8, /*agprs=*/0, /*accum_base=*/0,
/*sgprs=*/8, /*private_bytes=*/12);
SemanticSpillFrame first_instruction(context);
const auto wide = first_instruction.allocate_dwords(4, /*byte_alignment=*/4);
ASSERT_TRUE(wide);
EXPECT_EQ(wide->byte_offset, 16u);
EXPECT_EQ(context.required_private_segment_fixed_size, 32u);
SemanticSpillFrame second_instruction(context);
const auto narrow = second_instruction.allocate_dwords(1, /*byte_alignment=*/4);
ASSERT_TRUE(narrow);
EXPECT_EQ(narrow->byte_offset, 16u);
EXPECT_EQ(context.required_private_segment_fixed_size, 32u);
}
TEST(SemanticScratchAllocator, FallsBackToNonForbiddenSpillVictim) {
// An instruction absent from the liveness snapshot has no proven-dead
// registers, which deliberately drives the allocator through its spill tier.
Instruction inst("scratch_test", nullptr);
std::vector<BasicBlock *> blocks;
const ExecMaskAnalysis exec(KernelBlockScope(blocks), /*wave_size=*/64);
LivenessAnalysis liveness(blocks, std::make_unique<ExecMaskAnalysis>(exec));
TranslationContext context(/*vgprs=*/8, /*agprs=*/0, /*accum_base=*/0,
/*sgprs=*/8, /*private_bytes=*/20);
SemanticScratchAllocator allocator(inst, liveness, context,
Cdna3ScratchEmitter::allocation_policy());
SemanticScratchRequest request;
request.count = 2;
request.alignment = 2;
request.forbidden.expand({RegClass::VGPR, 0, 2});
request.preferred_victim_base = 0;
const SemanticScratchResult result = allocator.acquire_vgprs(request);
ASSERT_TRUE(result);
ASSERT_TRUE(result.lease->spilled);
EXPECT_EQ(result.lease->base, 2u);
EXPECT_EQ(result.lease->spill_offset, 32u);
EXPECT_EQ(context.required_private_segment_fixed_size, 40u);
}
TEST(SemanticScratchAllocator, PrefersKernelUnusedOverSiteDeadVgpr) {
auto blocks = build_scratch_test_blocks();
auto scope = scratch_test_scope(blocks);
const ExecMaskAnalysis exec{KernelBlockScope(scope), /*wave_size=*/64};
const LivenessAnalysis liveness{KernelBlockScope(scope),
std::make_unique<ExecMaskAnalysis>(exec)};
auto site = blocks.front()->instructions().begin();
++site;
ASSERT_NE(site, blocks.front()->instructions().end());
TranslationContext context(/*vgprs=*/8, /*agprs=*/0, /*accum_base=*/0,
/*sgprs=*/8, /*private_bytes=*/64);
SemanticScratchAllocator allocator(*site, liveness, context,
Cdna3ScratchEmitter::allocation_policy());
SemanticScratchRequest request;
request.count = 1;
const SemanticScratchResult result = allocator.acquire_vgprs(request);
ASSERT_TRUE(result);
EXPECT_FALSE(result.lease->spilled);
EXPECT_EQ(result.lease->base, 1u);
EXPECT_FALSE(liveness.has_materialized_cfg_liveness())
<< "kernel-unused allocation must not force backward CFG liveness";
}
TEST(SemanticScratchAllocator, KernelUnusedSearchSkipsForbiddenWindow) {
auto blocks = build_scratch_test_blocks();
auto scope = scratch_test_scope(blocks);
const ExecMaskAnalysis exec{KernelBlockScope(scope), /*wave_size=*/64};
const LivenessAnalysis liveness{KernelBlockScope(scope),
std::make_unique<ExecMaskAnalysis>(exec)};
auto site = blocks.front()->instructions().begin();
++site;
ASSERT_NE(site, blocks.front()->instructions().end());
TranslationContext context(/*vgprs=*/8, /*agprs=*/0, /*accum_base=*/0,
/*sgprs=*/8, /*private_bytes=*/64);
SemanticScratchAllocator allocator(*site, liveness, context,
Cdna3ScratchEmitter::allocation_policy());
SemanticScratchRequest request;
request.count = 1;
request.forbidden.expand({RegClass::VGPR, 1, 3});
const SemanticScratchResult result = allocator.acquire_vgprs(request);
ASSERT_TRUE(result);
EXPECT_FALSE(result.lease->spilled);
EXPECT_EQ(result.lease->base, 4u);
EXPECT_FALSE(liveness.has_materialized_cfg_liveness());
}
TEST(SemanticScratchAllocator, GprIndexModeWriteBypassesKernelUnusedTier) {
constexpr uint16_t kModeGprIdxEnableHwreg = 1u | (27u << 6);
constexpr auto setreg =
cdna4::build_sopk(cdna4::kSSetregB32Sopk, {.simm16 = kModeGprIdxEnableHwreg, .sdst = 0});
constexpr auto move = cdna4::build_vop1(cdna4::kVMovB32Vop1, {.src0 = 256, .vdst = 0});
constexpr auto end = cdna4::build_sopp(cdna4::kSEndpgmSopp);
constexpr std::array words = {setreg[0], move[0], end[0]};
ScratchTestCodeObject code(words);
auto decoder = Decoder::create(ROCJITSU_CODE_ARCH_CDNA4);
ASSERT_NE(decoder, nullptr);
auto blocks = BasicBlock::build(code, *decoder, ROCJITSU_CODE_ARCH_CDNA4);
ASSERT_EQ(blocks.size(), 1u);
auto scope = scratch_test_scope(blocks);
LivenessAnalysisOptions options;
options.arch = ROCJITSU_CODE_ARCH_CDNA4;
options.text = scratch_test_text(code);
const ExecMaskAnalysis exec{KernelBlockScope(scope), /*wave_size=*/64};
const LivenessAnalysis liveness{KernelBlockScope(scope), std::make_unique<ExecMaskAnalysis>(exec),
options};
auto site = blocks.front()->instructions().begin();
std::advance(site, 2);
ASSERT_NE(site, blocks.front()->instructions().end());
TranslationContext context(/*vgprs=*/2, /*agprs=*/0, /*accum_base=*/0,
/*sgprs=*/8, /*private_bytes=*/64);
SemanticScratchAllocator allocator(*site, liveness, context,
Cdna3ScratchEmitter::allocation_policy());
SemanticScratchRequest request;
request.count = 1;
request.allow_spill = false;
const SemanticScratchResult result = allocator.acquire_vgprs(request);
ASSERT_TRUE(result);
EXPECT_FALSE(result.lease->spilled);
EXPECT_EQ(result.lease->base, 0u);
EXPECT_TRUE(liveness.has_materialized_cfg_liveness())
<< "GPR indexing must bypass the kernel-unused tier";
}
TEST(SemanticScratchAllocator, SiteDeadTierCanGrowDescriptor) {
auto blocks = build_scratch_test_blocks();
auto scope = scratch_test_scope(blocks);
const ExecMaskAnalysis exec{KernelBlockScope(scope), /*wave_size=*/64};
const LivenessAnalysis liveness{KernelBlockScope(scope),
std::make_unique<ExecMaskAnalysis>(exec)};
const Instruction &site = *blocks.front()->instructions().begin();
// The sole descriptor-allocated register v0 is read at this site, so the
// kernel-unused tier has no in-descriptor choice and point liveness grows the
// allocation to v1.
TranslationContext context(/*vgprs=*/1, /*agprs=*/0, /*accum_base=*/0,
/*sgprs=*/8, /*private_bytes=*/64);
SemanticScratchAllocator allocator(site, liveness, context,
Cdna3ScratchEmitter::allocation_policy());
SemanticScratchRequest request;
request.count = 1;
request.allow_spill = false;
const SemanticScratchResult result = allocator.acquire_vgprs(request);
ASSERT_TRUE(result);
EXPECT_FALSE(result.lease->spilled);
EXPECT_EQ(result.lease->base, 1u);
EXPECT_EQ(context.required_vgpr_count, 2u);
EXPECT_TRUE(liveness.has_materialized_cfg_liveness());
}
TEST(SemanticScratchAllocator, ReportsTargetSpillOffsetLimit) {
Instruction inst("scratch_test", nullptr);
std::vector<BasicBlock *> blocks;
const ExecMaskAnalysis exec(KernelBlockScope(blocks), /*wave_size=*/64);
LivenessAnalysis liveness(blocks, std::make_unique<ExecMaskAnalysis>(exec));
TranslationContext context(/*vgprs=*/8, /*agprs=*/0, /*accum_base=*/0,
/*sgprs=*/8, /*private_bytes=*/32);
SemanticScratchAllocator allocator(
inst, liveness, context,
SemanticScratchPolicy{.max_vgprs = 256, .max_spill_dword_offset = 31});
SemanticScratchRequest request;
request.count = 1;
const SemanticScratchResult result = allocator.acquire_vgprs(request);
EXPECT_FALSE(result);
EXPECT_EQ(result.failure, SemanticScratchFailure::SpillOffsetUnencodable);
EXPECT_EQ(context.required_private_segment_fixed_size, 32u);
}
TEST(SemanticScratchAllocator, RejectsSpillVictimInDynamicStackKernel) {
Instruction inst("scratch_test", nullptr);
std::vector<BasicBlock *> blocks;
const ExecMaskAnalysis exec(KernelBlockScope(blocks), /*wave_size=*/64);
LivenessAnalysis liveness(blocks, std::make_unique<ExecMaskAnalysis>(exec));
// With no live-before snapshot, no register window is proven free, so the
// allocator must borrow a victim and exercise the spill policy.
ASSERT_FALSE(liveness.has_live_before(inst));
TranslationContext context(/*vgprs=*/8, /*agprs=*/0, /*accum_base=*/0,
/*sgprs=*/8, /*private_bytes=*/32);
context.uses_dynamic_stack = true;
SemanticScratchAllocator allocator(
inst, liveness, context, SemanticScratchPolicy{.max_vgprs = 8, .max_spill_dword_offset = 0});
SemanticScratchRequest request;
request.count = 1;
const SemanticScratchResult result = allocator.acquire_vgprs(request);
EXPECT_FALSE(result);
EXPECT_EQ(result.failure, SemanticScratchFailure::DynamicStackUnsupported);
EXPECT_EQ(context.required_private_segment_fixed_size, 32u);
}
TEST(SemanticScratchAllocator, AllowsFreeWindowInDynamicStackKernel) {
auto blocks = build_scratch_test_blocks();
std::vector<BasicBlock *> scope;
for (const auto &block : blocks)
scope.push_back(block.get());
const Instruction &inst = *blocks.front()->instructions().begin();
const ExecMaskAnalysis exec(KernelBlockScope(scope), /*wave_size=*/64);
LivenessAnalysis liveness(scope, std::make_unique<ExecMaskAnalysis>(exec));
TranslationContext context(/*vgprs=*/8, /*agprs=*/0, /*accum_base=*/0,
/*sgprs=*/8, /*private_bytes=*/32);
context.uses_dynamic_stack = true;
SemanticScratchAllocator allocator(
inst, liveness, context, SemanticScratchPolicy{.max_vgprs = 8, .max_spill_dword_offset = 0});
SemanticScratchRequest request;
request.count = 1;
const SemanticScratchResult result = allocator.acquire_vgprs(request);
ASSERT_TRUE(result);
EXPECT_EQ(result.failure, SemanticScratchFailure::None);
EXPECT_FALSE(result.lease->spilled);
EXPECT_EQ(result.lease->base, 1u);
EXPECT_EQ(context.required_private_segment_fixed_size, 32u);
}
TEST(Cdna3ScratchEmitter, MaterializesSaveAndRestoreSequences) {
const SemanticScratchLease lease{
.reg_class = RegClass::VGPR, .base = 6, .count = 2, .spilled = true, .spill_offset = 48};
std::vector<uint32_t> save;
std::vector<uint32_t> restore;
ASSERT_TRUE(Cdna3ScratchEmitter::append_save(save, lease));
ASSERT_TRUE(Cdna3ScratchEmitter::append_restore(restore, lease));
ASSERT_EQ(save.size(), 5u);
ASSERT_EQ(restore.size(), 5u);
cdna3::FlatScratchMachineInst first_save{};
cdna3::FlatScratchMachineInst second_save{};
cdna3::FlatScratchMachineInst first_restore{};
std::memcpy(&first_save, save.data(), sizeof(first_save));
std::memcpy(&second_save, save.data() + 2, sizeof(second_save));
std::memcpy(&first_restore, restore.data(), sizeof(first_restore));
EXPECT_EQ(first_save.op, 28u);
EXPECT_EQ(first_save.data, 6u);
EXPECT_EQ(first_save.offset, 48u);
EXPECT_EQ(second_save.data, 7u);
EXPECT_EQ(second_save.offset, 52u);
EXPECT_EQ(first_restore.op, 20u);
EXPECT_EQ(first_restore.vdst, 6u);
EXPECT_EQ(first_restore.offset, 48u);
}
} // namespace
} // namespace rocjitsu