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

444 lines
17 KiB
C++

// Copyright © Advanced Micro Devices, Inc., or its affiliates.
// SPDX-License-Identifier: MIT
#include <gtest/gtest.h>
#include <hipdnn_frontend/Graph.hpp>
#include <hipdnn_frontend/autotune/AutotuneBenchmark.hpp>
#include <hipdnn_frontend/autotune/AutotuneTypes.hpp>
#include <hipdnn_frontend/autotune/BenchmarkStatistics.hpp>
#include <hipdnn_frontend/autotune/KnobConstants.hpp>
#include <hipdnn_frontend/autotune/PlanSpec.hpp>
#include <hipdnn_frontend/autotune/TimedRunLoop.hpp>
#include <cmath>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
using namespace hipdnn_frontend;
using namespace hipdnn_frontend::autotune;
// ============================================================================
// Benchmarking Knob Stripping
// ============================================================================
TEST(TestAutotune, BenchmarkingKnobNameIsGlobalBenchmarking)
{
EXPECT_EQ(autotune::detail::BENCHMARKING_KNOB_NAME, "global.benchmarking");
}
// ============================================================================
// AutotuneConfig Validation Tests
// ============================================================================
TEST(TestAutotune, ConfigValidationNegativeWarmup)
{
hipdnn_frontend::graph::Graph g;
AutotuneConfig config;
config.warmupIterations = -1;
// Config validation in autotuneImpl fires before handle/graph checks,
// so we can pass a null handle and empty compiled plans.
const std::unordered_map<int64_t, void*> variantPack = {{0, nullptr}};
auto err = g.autotune(nullptr, variantPack, nullptr, int64_t{0}, config);
EXPECT_TRUE(err.is_bad());
EXPECT_NE(err.get_message().find("warmupIterations"), std::string::npos);
}
TEST(TestAutotune, ConfigValidationNegativeTimedIterations)
{
hipdnn_frontend::graph::Graph g;
AutotuneConfig config;
config.strategy = AutotuneStrategy::FIXED_AVERAGE;
config.timedIterations = 0; // Must be >= 1 for FIXED_AVERAGE
const std::unordered_map<int64_t, void*> variantPack = {{0, nullptr}};
auto err = g.autotune(nullptr, variantPack, nullptr, int64_t{0}, config);
EXPECT_TRUE(err.is_bad());
EXPECT_NE(err.get_message().find("timedIterations"), std::string::npos);
}
TEST(TestAutotune, ConfigValidationWindowSizeTooSmall)
{
hipdnn_frontend::graph::Graph g;
AutotuneConfig config;
config.windowSize = 1; // Must be >= 2
const std::unordered_map<int64_t, void*> variantPack = {{0, nullptr}};
auto err = g.autotune(nullptr, variantPack, nullptr, int64_t{0}, config);
EXPECT_TRUE(err.is_bad());
EXPECT_NE(err.get_message().find("windowSize"), std::string::npos);
}
TEST(TestAutotune, ConfigValidationStabilityThresholdOutOfBounds)
{
hipdnn_frontend::graph::Graph g;
const std::unordered_map<int64_t, void*> variantPack = {{0, nullptr}};
// Must be in (0.0, 1.0) exclusive
{
AutotuneConfig config;
config.stabilityThreshold = 0.0f;
auto err = g.autotune(nullptr, variantPack, nullptr, int64_t{0}, config);
EXPECT_TRUE(err.is_bad());
EXPECT_NE(err.get_message().find("stabilityThreshold"), std::string::npos);
}
{
AutotuneConfig config;
config.stabilityThreshold = 1.0f;
auto err = g.autotune(nullptr, variantPack, nullptr, int64_t{0}, config);
EXPECT_TRUE(err.is_bad());
EXPECT_NE(err.get_message().find("stabilityThreshold"), std::string::npos);
}
{
AutotuneConfig config;
config.stabilityThreshold = -0.5f;
auto err = g.autotune(nullptr, variantPack, nullptr, int64_t{0}, config);
EXPECT_TRUE(err.is_bad());
EXPECT_NE(err.get_message().find("stabilityThreshold"), std::string::npos);
}
}
// ============================================================================
// EngineConfigInfo Tests
// ============================================================================
TEST(TestAutotune, EngineConfigInfoDefaults)
{
const EngineConfigInfo info;
EXPECT_EQ(info.engineId, -1);
EXPECT_TRUE(info.engineName.empty());
EXPECT_TRUE(info.knobs.empty());
EXPECT_FALSE(info.supportsExhaustive);
EXPECT_EQ(info.estimatedWorkspaceSize, 0);
}
// ============================================================================
// Timed-run loop tests (FIXED_AVERAGE / RUN_UNTIL_STABLE)
//
// These drive the real production loop helpers (runUntilStable /
// runFixedAverage) with a scripted timing sequence, replacing the prior cases
// that re-implemented the convergence gating inline against literal arrays.
// Fixed params: windowSize=3, stabilityThreshold=0.05,
// maxIterations=10. Every row asserts an EXACT iteration count so an
// off-by-one in the window slice cannot pass.
// ============================================================================
namespace
{
// A scripted timing source: returns the next value from a fixed sequence,
// optionally returning a bad Error on a designated iteration to exercise the
// failure path.
struct ScriptedTimer
{
std::vector<float> values;
int failOnIteration = -1; // 0-based; -1 = never fail
int callCount = 0;
Error operator()(float& elapsed)
{
if(callCount == failOnIteration)
{
++callCount;
return {ErrorCode::HIPDNN_BACKEND_ERROR, "scripted failure"};
}
elapsed = values[static_cast<size_t>(callCount) % values.size()];
++callCount;
return {ErrorCode::OK, ""};
}
};
constexpr int WINDOW_SIZE = 3;
constexpr float STABILITY_THRESHOLD = 0.05f;
constexpr int MAX_ITERATIONS = 10;
auto noopRunUntilStableLog = [](int, float, float, bool) {};
auto noopFixedAverageLog = [](int, float) {};
} // namespace
TEST(TestAutotune, RunUntilStableConvergesAndExitsEarly)
{
ScriptedTimer timer{{10.0f}, -1, 0};
auto outcome = autotune::detail::runUntilStable(
MAX_ITERATIONS, WINDOW_SIZE, STABILITY_THRESHOLD, timer, noopRunUntilStableLog);
EXPECT_TRUE(outcome.converged);
EXPECT_FALSE(outcome.benchmarkFailed);
EXPECT_EQ(static_cast<int>(outcome.timings.size()), 3);
}
TEST(TestAutotune, RunUntilStableNeverConvergesHitsCap)
{
// Alternating values keep the trailing-window CoV above the threshold.
ScriptedTimer timer{{10.0f, 20.0f}, -1, 0};
auto outcome = autotune::detail::runUntilStable(
MAX_ITERATIONS, WINDOW_SIZE, STABILITY_THRESHOLD, timer, noopRunUntilStableLog);
EXPECT_FALSE(outcome.converged);
EXPECT_FALSE(outcome.benchmarkFailed);
EXPECT_EQ(static_cast<int>(outcome.timings.size()), MAX_ITERATIONS);
}
TEST(TestAutotune, RunUntilStableConvergesLate)
{
// First window {5,9,5} is noisy; later window {10,10,10} converges at iter 6.
ScriptedTimer timer{{5.0f, 9.0f, 5.0f, 10.0f, 10.0f, 10.0f}, -1, 0};
auto outcome = autotune::detail::runUntilStable(
MAX_ITERATIONS, WINDOW_SIZE, STABILITY_THRESHOLD, timer, noopRunUntilStableLog);
EXPECT_TRUE(outcome.converged);
EXPECT_EQ(static_cast<int>(outcome.timings.size()), 6);
}
TEST(TestAutotune, RunUntilStableFailureMidLoopBreaks)
{
// Fail on iteration index 3 (the 4th call): 3 timings recorded, loop broke.
// Use an alternating sequence so the first trailing window {10,20,10} has a
// high CoV and does NOT converge before the designated failure iteration -
// a constant sequence would converge at iter 2 and never reach the failure.
ScriptedTimer timer{{10.0f, 20.0f}, 3, 0};
auto outcome = autotune::detail::runUntilStable(
MAX_ITERATIONS, WINDOW_SIZE, STABILITY_THRESHOLD, timer, noopRunUntilStableLog);
EXPECT_TRUE(outcome.benchmarkFailed);
EXPECT_EQ(static_cast<int>(outcome.timings.size()), 3);
EXPECT_NE(outcome.errorMessage.find("scripted failure"), std::string::npos);
}
TEST(TestAutotune, RunFixedAverageRunsAllIterations)
{
ScriptedTimer timer{{7.0f, 8.0f, 9.0f}, -1, 0};
auto outcome
= autotune::detail::runFixedAverage(/*timedIterations=*/10, timer, noopFixedAverageLog);
EXPECT_TRUE(outcome.converged);
EXPECT_FALSE(outcome.benchmarkFailed);
EXPECT_EQ(static_cast<int>(outcome.timings.size()), 10);
}
TEST(TestAutotune, RunFixedAverageFailureMidLoopBreaks)
{
ScriptedTimer timer{{7.0f, 8.0f, 9.0f}, 2, 0};
auto outcome
= autotune::detail::runFixedAverage(/*timedIterations=*/5, timer, noopFixedAverageLog);
EXPECT_FALSE(outcome.converged);
EXPECT_TRUE(outcome.benchmarkFailed);
ASSERT_EQ(outcome.timings.size(), 2u);
EXPECT_FLOAT_EQ(outcome.timings[0], 7.0f);
EXPECT_FLOAT_EQ(outcome.timings[1], 8.0f);
EXPECT_NE(outcome.errorMessage.find("iteration 2"), std::string::npos);
EXPECT_NE(outcome.errorMessage.find("scripted failure"), std::string::npos);
}
TEST(TestAutotune, RunFixedAverageInvokesCallbackForEachSuccessfulIteration)
{
ScriptedTimer timer{{4.0f, 5.0f, 6.0f}, -1, 0};
std::vector<int> callbackIterations;
std::vector<float> callbackElapsedMs;
auto onIteration = [&](int iteration, float elapsedMs) {
callbackIterations.push_back(iteration);
callbackElapsedMs.push_back(elapsedMs);
};
auto outcome = autotune::detail::runFixedAverage(/*timedIterations=*/3, timer, onIteration);
EXPECT_TRUE(outcome.converged);
EXPECT_FALSE(outcome.benchmarkFailed);
EXPECT_EQ(callbackIterations, (std::vector<int>{0, 1, 2}));
ASSERT_EQ(callbackElapsedMs.size(), 3u);
EXPECT_FLOAT_EQ(callbackElapsedMs[0], 4.0f);
EXPECT_FLOAT_EQ(callbackElapsedMs[1], 5.0f);
EXPECT_FLOAT_EQ(callbackElapsedMs[2], 6.0f);
}
TEST(TestAutotune, RunUntilStableReportsCovValidityToCallback)
{
ScriptedTimer timer{{10.0f}, -1, 0};
std::vector<bool> covValidByIteration;
std::vector<float> covByIteration;
auto onIteration = [&](int, float, float cov, bool covValid) {
covValidByIteration.push_back(covValid);
covByIteration.push_back(cov);
};
auto outcome = autotune::detail::runUntilStable(
MAX_ITERATIONS, WINDOW_SIZE, STABILITY_THRESHOLD, timer, onIteration);
EXPECT_TRUE(outcome.converged);
ASSERT_EQ(covValidByIteration.size(), 3u);
EXPECT_FALSE(covValidByIteration[0]);
EXPECT_FALSE(covValidByIteration[1]);
EXPECT_TRUE(covValidByIteration[2]);
EXPECT_FLOAT_EQ(covByIteration[0], 0.0f);
EXPECT_FLOAT_EQ(covByIteration[1], 0.0f);
EXPECT_FLOAT_EQ(covByIteration[2], 0.0f);
}
// ============================================================================
// D2: maxIterations >= windowSize validation for RUN_UNTIL_STABLE
// ============================================================================
TEST(TestAutotune, MaxIterationsLessThanWindowSizeIsDetectable)
{
hipdnn_frontend::graph::Graph g;
AutotuneConfig config;
config.strategy = AutotuneStrategy::RUN_UNTIL_STABLE;
config.maxIterations = 3;
config.windowSize = 5;
const std::unordered_map<int64_t, void*> variantPack = {{0, nullptr}};
auto err = g.autotune(nullptr, variantPack, nullptr, int64_t{0}, config);
EXPECT_TRUE(err.is_bad());
EXPECT_NE(err.get_message().find("maxIterations"), std::string::npos);
}
TEST(TestAutotune, MaxIterationsCheckOnlyForRunUntilStable)
{
// For FIXED_AVERAGE, maxIterations < windowSize should not be an error.
// Config validation in autotuneImpl only checks maxIterations vs windowSize
// for RUN_UNTIL_STABLE, so FIXED_AVERAGE with maxIterations < windowSize
// should pass config validation and fail later on a different check.
hipdnn_frontend::graph::Graph g;
AutotuneConfig config;
config.strategy = AutotuneStrategy::FIXED_AVERAGE;
config.maxIterations = 3;
config.windowSize = 5;
const std::unordered_map<int64_t, void*> variantPack = {{0, nullptr}};
auto err = g.autotune(nullptr, variantPack, nullptr, int64_t{0}, config);
// FIXED_AVERAGE must pass the maxIterations>=windowSize validation (that
// gate is RUN_UNTIL_STABLE-only). The call still fails for an unrelated
// reason (null handle), but the error must NOT be the maxIterations check.
EXPECT_EQ(err.get_message().find("maxIterations"), std::string::npos)
<< "FIXED_AVERAGE must not trigger the maxIterations validation: " << err.get_message();
}
// ============================================================================
// Strategy-scoped parameter validation: timedIterations is only required for
// FIXED_AVERAGE, maxIterations only for RUN_UNTIL_STABLE.
// warmupIterations is required for all strategies.
// ============================================================================
TEST(TestAutotune, FixedAverageIgnoresMaxIterations)
{
// FIXED_AVERAGE never reads maxIterations, so maxIterations=0 must pass
// config validation.
hipdnn_frontend::graph::Graph g;
AutotuneConfig config;
config.strategy = AutotuneStrategy::FIXED_AVERAGE;
config.maxIterations = 0;
const std::unordered_map<int64_t, void*> variantPack = {{0, nullptr}};
auto err = g.autotune(nullptr, variantPack, nullptr, int64_t{0}, config);
// Config validation passes; the call advances to the null-handle check that
// immediately follows the validation block, proving maxIterations=0 was
// accepted for FIXED_AVERAGE.
EXPECT_EQ(err.code, ErrorCode::INVALID_VALUE);
EXPECT_NE(err.get_message().find("handle must not be null"), std::string::npos)
<< "FIXED_AVERAGE must pass config validation and fail on the null handle: "
<< err.get_message();
EXPECT_EQ(err.get_message().find("maxIterations"), std::string::npos)
<< "FIXED_AVERAGE must not trigger the maxIterations validation: " << err.get_message();
}
TEST(TestAutotune, FixedAverageRejectsZeroTimedIterations)
{
// Regression guard: FIXED_AVERAGE with timedIterations=0 must still be
// rejected (an empty timings vector would otherwise throw in computeMean).
hipdnn_frontend::graph::Graph g;
AutotuneConfig config;
config.strategy = AutotuneStrategy::FIXED_AVERAGE;
config.timedIterations = 0;
const std::unordered_map<int64_t, void*> variantPack = {{0, nullptr}};
auto err = g.autotune(nullptr, variantPack, nullptr, int64_t{0}, config);
EXPECT_TRUE(err.is_bad());
EXPECT_NE(err.get_message().find("timedIterations"), std::string::npos);
}
TEST(TestAutotune, RunUntilStableRejectsZeroMaxIterations)
{
// Regression guard: RUN_UNTIL_STABLE with maxIterations=0 must still be
// rejected.
hipdnn_frontend::graph::Graph g;
AutotuneConfig config;
config.strategy = AutotuneStrategy::RUN_UNTIL_STABLE;
config.maxIterations = 0;
const std::unordered_map<int64_t, void*> variantPack = {{0, nullptr}};
auto err = g.autotune(nullptr, variantPack, nullptr, int64_t{0}, config);
EXPECT_TRUE(err.is_bad());
EXPECT_NE(err.get_message().find("maxIterations"), std::string::npos);
}
TEST(TestAutotune, WarmupValidationAppliesToAllStrategies)
{
// warmupIterations stays unconditional across every strategy.
const std::unordered_map<int64_t, void*> variantPack = {{0, nullptr}};
for(auto strategy : {AutotuneStrategy::FIXED_AVERAGE, AutotuneStrategy::RUN_UNTIL_STABLE})
{
hipdnn_frontend::graph::Graph g;
AutotuneConfig config;
config.strategy = strategy;
config.warmupIterations = -1;
auto err = g.autotune(nullptr, variantPack, nullptr, int64_t{0}, config);
EXPECT_TRUE(err.is_bad());
EXPECT_NE(err.get_message().find("warmupIterations"), std::string::npos);
}
}
// ============================================================================
// rankAndSelectWinner
// ============================================================================
TEST(TestAutotune, RankAndSelectWinnerSortsSucceededAndSelectsFastest)
{
AutotuneResult slow;
slow.engineId = 10;
slow.engineName = "slow";
slow.minTimeMs = 5.0f;
slow.succeeded = true;
slow.compiledPlanIndex = 0;
AutotuneResult fast;
fast.engineId = 20;
fast.engineName = "fast";
fast.minTimeMs = 1.0f;
fast.succeeded = true;
fast.compiledPlanIndex = 1;
AutotuneResult medium;
medium.engineId = 30;
medium.engineName = "medium";
medium.minTimeMs = 3.0f;
medium.succeeded = true;
medium.compiledPlanIndex = 2;
AutotuneResult failed;
failed.engineId = 40;
failed.engineName = "failed";
failed.succeeded = false;
failed.compiledPlanIndex = 3;
std::vector<AutotuneResult> results = {slow, fast, medium, failed};
const AutotuneConfig config;
size_t activePlanIndex = SIZE_MAX;
auto err = autotune::detail::rankAndSelectWinner(results, config, activePlanIndex);
ASSERT_TRUE(err.is_good()) << err.get_message();
ASSERT_EQ(results.size(), 4u);
EXPECT_EQ(results[0].engineId, 20);
EXPECT_EQ(results[0].rank, 0);
EXPECT_EQ(results[1].engineId, 30);
EXPECT_EQ(results[1].rank, 1);
EXPECT_EQ(results[2].engineId, 10);
EXPECT_EQ(results[2].rank, 2);
EXPECT_FALSE(results[3].succeeded);
EXPECT_EQ(results[3].engineId, 40);
EXPECT_EQ(results[3].rank, -1);
EXPECT_EQ(activePlanIndex, 1u);
}