Files
TheRock/rocm-libraries/projects/hipdnn/backend/tests/TestSelectionHeuristic.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

474 lines
18 KiB
C++

// Copyright © Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
/**
* @file TestSelectionHeuristic.cpp
* @brief Unit tests for SelectionHeuristic RAII wrapper
*
* Tests the C++ facade that wraps hipdnnHeuristicPolicyDescriptor_t lifecycle
* and provides clean API over the heuristic plugin C ABI.
*/
#include "heuristics/SelectionHeuristic.hpp"
#include "descriptors/mocks/MockHeuristicPlugin.hpp"
#include "descriptors/mocks/MockHeuristicPluginResourceManager.hpp"
#include "plugin/HeuristicPlugin.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <hipdnn_plugin_sdk/HeuristicsPluginApi.h>
using namespace hipdnn_backend::heuristics;
using namespace hipdnn_backend::plugin;
using ::testing::NiceMock;
class TestSelectionHeuristic : public ::testing::Test
{
protected:
void SetUp() override
{
// Create a mock plugin handle (just a non-null pointer for testing)
_mockHandle = reinterpret_cast<hipdnnHeuristicHandle_t>(this);
_mockPlugin = std::make_unique<MockHeuristicPlugin>();
_mockResourceManager = std::make_shared<NiceMock<MockHeuristicPluginResourceManager>>();
}
// Wires the mock resource manager to return _mockHandle / _mockPlugin.get()
// for the test policy ID. Use AnyNumber so cleanup-time lookups in the
// SelectionHeuristic destructor (and move ops) are also satisfied.
void wireResourceManager()
{
EXPECT_CALL(*_mockResourceManager, getHeuristicHandleForPolicyId(_policyId))
.WillRepeatedly(::testing::Return(_mockHandle));
EXPECT_CALL(*_mockResourceManager, getPluginForPolicyId(_policyId))
.WillRepeatedly(::testing::Return(_mockPlugin.get()));
}
hipdnnHeuristicHandle_t _mockHandle = nullptr;
std::unique_ptr<MockHeuristicPlugin> _mockPlugin;
std::shared_ptr<NiceMock<MockHeuristicPluginResourceManager>> _mockResourceManager;
int64_t _policyId = 12345;
};
// ========== Constructor Tests ==========
TEST_F(TestSelectionHeuristic, ConstructorWithValidInputs)
{
wireResourceManager();
// Expect createPolicyDescriptor to be called
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0x1234);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor)).Times(1);
// Should not throw
const SelectionHeuristic heuristic(_mockResourceManager, _policyId);
}
TEST_F(TestSelectionHeuristic, ConstructorThrowsOnNullResourceManager)
{
EXPECT_THROW(
{
SelectionHeuristic heuristic(nullptr, _policyId); // NOLINT(misc-const-correctness)
},
hipdnn_backend::HipdnnException);
}
TEST_F(TestSelectionHeuristic, ConstructorThrowsWhenPolicyHasNoHandle)
{
// Manager reports no handle for this policy ID
EXPECT_CALL(*_mockResourceManager, getHeuristicHandleForPolicyId(_policyId))
.WillRepeatedly(::testing::Return(nullptr));
EXPECT_THROW(
{ const SelectionHeuristic heuristic(_mockResourceManager, _policyId); },
hipdnn_backend::HipdnnException);
}
TEST_F(TestSelectionHeuristic, ConstructorThrowsWhenPolicyHasNoPlugin)
{
// Manager reports a handle but no plugin (defensive check)
EXPECT_CALL(*_mockResourceManager, getHeuristicHandleForPolicyId(_policyId))
.WillRepeatedly(::testing::Return(_mockHandle));
EXPECT_CALL(*_mockResourceManager, getPluginForPolicyId(_policyId))
.WillRepeatedly(::testing::Return(nullptr));
EXPECT_THROW(
{ const SelectionHeuristic heuristic(_mockResourceManager, _policyId); },
hipdnn_backend::HipdnnException);
}
// ========== Move Semantics Tests ==========
TEST_F(TestSelectionHeuristic, MoveConstructor)
{
wireResourceManager();
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0x5678);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
// Destroy should be called exactly once (when moved-to object is destroyed)
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor)).Times(1);
// Regression: the move must carry _inputEngineIds too, otherwise the
// validator in getSortedEngineIds sees an empty candidate set and rejects
// every legitimate plugin output with HIPDNN_STATUS_PLUGIN_ERROR.
const std::vector<int64_t> inputIds = {1, 2, 3};
const std::vector<int64_t> sortedIds = {3, 1, 2};
EXPECT_CALL(*_mockPlugin, setEngineIds(mockDescriptor, ::testing::_, inputIds.size())).Times(1);
EXPECT_CALL(*_mockPlugin, getSortedEngineIds(mockDescriptor))
.WillOnce(::testing::Return(sortedIds));
{
SelectionHeuristic heuristic1(_mockResourceManager, _policyId);
heuristic1.setEngineIds(inputIds);
// Move construct
SelectionHeuristic heuristic2(std::move(heuristic1)); // NOLINT(misc-const-correctness)
// heuristic2 should now own the descriptor and the input-ID candidate set
EXPECT_EQ(heuristic2.getSortedEngineIds(), sortedIds);
// heuristic1 should be empty (moved-from state)
} // Both destructors called, but only heuristic2 has valid descriptor
}
TEST_F(TestSelectionHeuristic, MoveAssignment)
{
wireResourceManager();
auto mockDescriptor1 = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0x1111);
auto mockDescriptor2 = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0x2222);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor1))
.WillOnce(::testing::Return(mockDescriptor2));
// First descriptor destroyed during move assignment
// Second descriptor destroyed when moved-to object is destroyed
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor1)).Times(1);
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor2)).Times(1);
// Regression: the move must carry _inputEngineIds too, so the validator
// in getSortedEngineIds still has the candidate set after the assignment.
const std::vector<int64_t> inputIds = {10, 20, 30};
const std::vector<int64_t> sortedIds = {30, 20, 10};
EXPECT_CALL(*_mockPlugin, setEngineIds(mockDescriptor2, ::testing::_, inputIds.size()))
.Times(1);
EXPECT_CALL(*_mockPlugin, getSortedEngineIds(mockDescriptor2))
.WillOnce(::testing::Return(sortedIds));
{
SelectionHeuristic heuristic1(_mockResourceManager, _policyId);
SelectionHeuristic heuristic2(_mockResourceManager, _policyId);
heuristic2.setEngineIds(inputIds);
// Move assign
heuristic1 = std::move(heuristic2);
// heuristic1 should now own mockDescriptor2 and heuristic2's input IDs
// heuristic2 should be empty
// mockDescriptor1 should have been destroyed
EXPECT_EQ(heuristic1.getSortedEngineIds(), sortedIds);
}
}
TEST_F(TestSelectionHeuristic, MoveAssignmentSelfAssignment)
{
wireResourceManager();
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0x9999);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor)).Times(1);
{
SelectionHeuristic heuristic(_mockResourceManager, _policyId);
// Self-assignment should be safe (use reference to avoid warning)
SelectionHeuristic& heuristicRef = heuristic;
heuristic = std::move(heuristicRef);
// Descriptor should still be valid
}
}
// ========== API Tests ==========
TEST_F(TestSelectionHeuristic, SetEngineIds)
{
wireResourceManager();
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0xAAAA);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
std::vector<int64_t> testEngineIds = {1, 2, 3, 4, 5};
EXPECT_CALL(
*_mockPlugin,
setEngineIds(mockDescriptor, ::testing::Pointee(testEngineIds[0]), testEngineIds.size()))
.Times(1);
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor)).Times(1);
SelectionHeuristic heuristic(_mockResourceManager, _policyId);
heuristic.setEngineIds(testEngineIds);
}
TEST_F(TestSelectionHeuristic, SetSerializedGraph)
{
wireResourceManager();
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0xBBBB);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
std::vector<uint8_t> graphData = {0x01, 0x02, 0x03};
const hipdnnPluginConstData_t serializedGraph{graphData.data(), graphData.size()};
EXPECT_CALL(*_mockPlugin, setSerializedGraph(mockDescriptor, &serializedGraph)).Times(1);
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor)).Times(1);
SelectionHeuristic heuristic(_mockResourceManager, _policyId);
heuristic.setSerializedGraph(&serializedGraph);
}
TEST_F(TestSelectionHeuristic, FinalizeReturnsTrue)
{
wireResourceManager();
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0xCCCC);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
EXPECT_CALL(*_mockPlugin, finalize(mockDescriptor)).WillOnce(::testing::Return(true));
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor)).Times(1);
SelectionHeuristic heuristic(_mockResourceManager, _policyId);
EXPECT_TRUE(heuristic.finalize());
}
TEST_F(TestSelectionHeuristic, FinalizeReturnsFalse)
{
wireResourceManager();
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0xDDDD);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
EXPECT_CALL(*_mockPlugin, finalize(mockDescriptor)).WillOnce(::testing::Return(false));
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor)).Times(1);
SelectionHeuristic heuristic(_mockResourceManager, _policyId);
EXPECT_FALSE(heuristic.finalize());
}
TEST_F(TestSelectionHeuristic, GetSortedEngineIds)
{
wireResourceManager();
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0xEEEE);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
// The plugin output must be a permutation/subset of the IDs we hand in
// via setEngineIds — that's what SelectionHeuristic validates.
const std::vector<int64_t> inputIds = {1, 2, 3, 4, 5};
const std::vector<int64_t> expectedIds = {5, 4, 3, 2, 1};
EXPECT_CALL(*_mockPlugin, setEngineIds(mockDescriptor, ::testing::_, inputIds.size())).Times(1);
EXPECT_CALL(*_mockPlugin, getSortedEngineIds(mockDescriptor))
.WillOnce(::testing::Return(expectedIds));
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor)).Times(1);
SelectionHeuristic heuristic(_mockResourceManager, _policyId);
heuristic.setEngineIds(inputIds);
auto result = heuristic.getSortedEngineIds();
EXPECT_EQ(result, expectedIds);
}
TEST_F(TestSelectionHeuristic, GetSortedEngineIdsRejectsFabricatedId)
{
wireResourceManager();
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0xEEEE);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
const std::vector<int64_t> inputIds = {1, 2, 3};
// Plugin returns an ID (99) that wasn't in the candidate set.
const std::vector<int64_t> badIds = {2, 99, 1};
EXPECT_CALL(*_mockPlugin, setEngineIds(mockDescriptor, ::testing::_, inputIds.size())).Times(1);
EXPECT_CALL(*_mockPlugin, getSortedEngineIds(mockDescriptor))
.WillOnce(::testing::Return(badIds));
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor)).Times(1);
SelectionHeuristic heuristic(_mockResourceManager, _policyId);
heuristic.setEngineIds(inputIds);
EXPECT_THROW(heuristic.getSortedEngineIds(), hipdnn_backend::HipdnnException);
}
TEST_F(TestSelectionHeuristic, GetSortedEngineIdsRejectsDuplicates)
{
wireResourceManager();
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0xEEEE);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
const std::vector<int64_t> inputIds = {1, 2, 3};
const std::vector<int64_t> badIds = {1, 2, 2};
EXPECT_CALL(*_mockPlugin, setEngineIds(mockDescriptor, ::testing::_, inputIds.size())).Times(1);
EXPECT_CALL(*_mockPlugin, getSortedEngineIds(mockDescriptor))
.WillOnce(::testing::Return(badIds));
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor)).Times(1);
SelectionHeuristic heuristic(_mockResourceManager, _policyId);
heuristic.setEngineIds(inputIds);
EXPECT_THROW(heuristic.getSortedEngineIds(), hipdnn_backend::HipdnnException);
}
TEST_F(TestSelectionHeuristic, GetSortedEngineIdsRejectsOversizedOutput)
{
wireResourceManager();
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0xEEEE);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
const std::vector<int64_t> inputIds = {1, 2};
const std::vector<int64_t> badIds = {1, 2, 3};
EXPECT_CALL(*_mockPlugin, setEngineIds(mockDescriptor, ::testing::_, inputIds.size())).Times(1);
EXPECT_CALL(*_mockPlugin, getSortedEngineIds(mockDescriptor))
.WillOnce(::testing::Return(badIds));
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor)).Times(1);
SelectionHeuristic heuristic(_mockResourceManager, _policyId);
heuristic.setEngineIds(inputIds);
EXPECT_THROW(heuristic.getSortedEngineIds(), hipdnn_backend::HipdnnException);
}
TEST_F(TestSelectionHeuristic, GetSortedEngineIdsAcceptsProperSubset)
{
wireResourceManager();
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0xEEEE);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
// Plugin may decline some candidates and return a strict subset.
const std::vector<int64_t> inputIds = {1, 2, 3, 4, 5};
const std::vector<int64_t> expectedIds = {3, 1};
EXPECT_CALL(*_mockPlugin, setEngineIds(mockDescriptor, ::testing::_, inputIds.size())).Times(1);
EXPECT_CALL(*_mockPlugin, getSortedEngineIds(mockDescriptor))
.WillOnce(::testing::Return(expectedIds));
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor)).Times(1);
SelectionHeuristic heuristic(_mockResourceManager, _policyId);
heuristic.setEngineIds(inputIds);
EXPECT_EQ(heuristic.getSortedEngineIds(), expectedIds);
}
// ========== Lifetime Tests ==========
// Verifies the SelectionHeuristic keeps the resource manager alive even if the
// caller drops its own shared_ptr — this is the core memory-safety guarantee
// the shared_ptr-to-manager design provides.
TEST_F(TestSelectionHeuristic, KeepsResourceManagerAliveAcrossCallerRelease)
{
wireResourceManager();
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0xABCD);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
EXPECT_CALL(*_mockPlugin, finalize(mockDescriptor)).WillOnce(::testing::Return(true));
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor)).Times(1);
const std::weak_ptr<NiceMock<MockHeuristicPluginResourceManager>> weakManager
= _mockResourceManager;
SelectionHeuristic heuristic(_mockResourceManager, _policyId);
// Caller releases its strong reference; the slot should still hold one.
_mockResourceManager.reset();
EXPECT_FALSE(weakManager.expired());
// Operations through the slot still work because the manager is alive.
EXPECT_TRUE(heuristic.finalize());
}
// ========== Exception Safety Tests ==========
TEST_F(TestSelectionHeuristic, DestructorHandlesExceptionInCleanup)
{
wireResourceManager();
auto mockDescriptor = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0xFFFF);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor));
// Destructor should catch and suppress exceptions
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor))
.WillOnce(::testing::Throw(
hipdnn_backend::HipdnnException(HIPDNN_STATUS_INTERNAL_ERROR, "Cleanup failed")));
// Should not throw from destructor
{
const SelectionHeuristic heuristic(_mockResourceManager, _policyId);
}
}
TEST_F(TestSelectionHeuristic, MoveAssignmentHandlesExceptionInCleanup)
{
wireResourceManager();
auto mockDescriptor1 = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0x1001);
auto mockDescriptor2 = reinterpret_cast<hipdnnHeuristicPolicyDescriptor_t>(0x2002);
EXPECT_CALL(*_mockPlugin, createPolicyDescriptor(_mockHandle, _policyId))
.WillOnce(::testing::Return(mockDescriptor1))
.WillOnce(::testing::Return(mockDescriptor2));
// First descriptor cleanup throws during move assignment
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor1))
.WillOnce(::testing::Throw(std::runtime_error("Cleanup error")));
// Second descriptor cleanup succeeds
EXPECT_CALL(*_mockPlugin, destroyPolicyDescriptor(mockDescriptor2)).Times(1);
{
SelectionHeuristic heuristic1(_mockResourceManager, _policyId);
SelectionHeuristic heuristic2(_mockResourceManager, _policyId);
// Move assignment should not throw even though cleanup of old descriptor throws
heuristic1 = std::move(heuristic2);
}
}