From 5c57e1a59ca3c964cfd01fdcce6b8b6d205b1141 Mon Sep 17 00:00:00 2001 From: Myoungho Shin Date: Wed, 26 Aug 2026 22:25:37 -0700 Subject: [PATCH 1/3] fix(amd): preserve trace lifecycle correctness --- .../backends/amd/amd_capture_capabilities.cpp | 56 ++++++-- .../backends/amd/amd_capture_capabilities.hpp | 3 + .../backends/amd/rocprofiler_backend.cpp | 128 +++++++++++++++--- .../backends/amd/rocprofiler_backend.hpp | 9 +- .../amd/test_amd_profiling_policy.cpp | 52 +++++++ 5 files changed, 217 insertions(+), 31 deletions(-) diff --git a/include/gpufl/backends/amd/amd_capture_capabilities.cpp b/include/gpufl/backends/amd/amd_capture_capabilities.cpp index 249f5ab..fe7f8ae 100644 --- a/include/gpufl/backends/amd/amd_capture_capabilities.cpp +++ b/include/gpufl/backends/amd/amd_capture_capabilities.cpp @@ -172,21 +172,59 @@ CaptureCapabilitiesEvent BuildAmdCaptureCapabilitiesEvent( ? "not_requested" : (!input.trace_configured ? "skipped" - : (input.dropped_trace_records > 0 ? "partial" : "enabled")), + : (input.dropped_trace_records > 0 || + input.trace_buffer_flush_failures > 0 || + input.dropped_client_records > 0 + ? "partial" + : "enabled")), input.trace_configured ? "rocprofiler_buffer_tracing" : "disabled", !input.trace_configured ? (tracing_requested ? "rocprofiler_buffer_tracing_unavailable" : "not_selected") - : (input.dropped_trace_records > 0 - ? "rocprofiler_records_dropped" - : ""), + : (input.trace_buffer_flush_failures > 0 + ? "rocprofiler_buffer_flush_failed" + : (input.dropped_trace_records > 0 + ? "rocprofiler_records_dropped" + : (input.dropped_client_records > 0 + ? "gpufl_activity_queue_full" + : ""))), !input.trace_configured ? "ROCprofiler trace-buffer delivery was not active." - : (input.dropped_trace_records > 0 - ? "ROCprofiler reported " + - std::to_string(input.dropped_trace_records) + - " dropped trace record(s); this session is incomplete." - : "ROCprofiler reported no dropped trace records.")); + : (input.trace_buffer_flush_failures > 0 + ? "ROCprofiler trace-buffer flush failed " + + std::to_string(input.trace_buffer_flush_failures) + + " time(s); buffered activity may be incomplete or in the wrong segment." + : (input.dropped_trace_records > 0 + ? "ROCprofiler reported " + + std::to_string(input.dropped_trace_records) + + " dropped trace record(s); this session is incomplete." + : (input.dropped_client_records > 0 + ? "GPUFlight dropped " + + std::to_string(input.dropped_client_records) + + " trace record(s) because its activity queue was full." + : "ROCprofiler and GPUFlight reported complete trace delivery.")))); + + AddCapability( + event, "scope_correlation", tracing_requested, + !tracing_requested + ? "not_requested" + : (!input.trace_configured + ? "skipped" + : (input.scope_correlation_failures > 0 ? "partial" : "enabled")), + input.trace_configured ? "rocprofiler_external_correlation" : "disabled", + !input.trace_configured + ? (tracing_requested ? "rocprofiler_buffer_tracing_unavailable" + : "not_selected") + : (input.scope_correlation_failures > 0 + ? "rocprofiler_scope_correlation_failed" + : ""), + !input.trace_configured + ? "ROCprofiler scope correlation was not active." + : (input.scope_correlation_failures > 0 + ? "ROCprofiler scope correlation failed " + + std::to_string(input.scope_correlation_failures) + + " time(s); some trace rows may have no user scope." + : "ROCprofiler accepted every GPUFlight scope-correlation push and pop.")); return event; } diff --git a/include/gpufl/backends/amd/amd_capture_capabilities.hpp b/include/gpufl/backends/amd/amd_capture_capabilities.hpp index 23e3bdd..3f0e80d 100644 --- a/include/gpufl/backends/amd/amd_capture_capabilities.hpp +++ b/include/gpufl/backends/amd/amd_capture_capabilities.hpp @@ -16,7 +16,10 @@ struct AmdCaptureCapabilityInput { uint64_t memcpy_rows = 0; uint64_t profiling_sample_rows = 0; uint64_t dropped_trace_records = 0; + uint64_t dropped_client_records = 0; + uint64_t trace_buffer_flush_failures = 0; uint64_t unattributed_trace_records = 0; + uint64_t scope_correlation_failures = 0; }; CaptureCapabilitiesEvent BuildAmdCaptureCapabilitiesEvent( diff --git a/include/gpufl/backends/amd/rocprofiler_backend.cpp b/include/gpufl/backends/amd/rocprofiler_backend.cpp index 85554e2..310e387 100644 --- a/include/gpufl/backends/amd/rocprofiler_backend.cpp +++ b/include/gpufl/backends/amd/rocprofiler_backend.cpp @@ -119,14 +119,21 @@ void RocprofilerBackend::initialize(const MonitorOptions& opts) { kernel_rows_emitted_.store(0, std::memory_order_relaxed); memcpy_rows_emitted_.store(0, std::memory_order_relaxed); trace_records_dropped_.store(0, std::memory_order_relaxed); + trace_records_queue_dropped_.store(0, std::memory_order_relaxed); + trace_buffer_flush_failures_.store(0, std::memory_order_relaxed); trace_records_unattributed_.store(0, std::memory_order_relaxed); + scope_correlation_failures_.store(0, std::memory_order_relaxed); + next_scope_external_.store(1, std::memory_order_relaxed); { std::lock_guard lock(capture_capabilities_mutex_); capture_capabilities_session_id_.clear(); capability_kernel_rows_baseline_ = 0; capability_memcpy_rows_baseline_ = 0; capability_dropped_records_baseline_ = 0; + capability_queue_dropped_records_baseline_ = 0; + capability_buffer_flush_failures_baseline_ = 0; capability_unattributed_records_baseline_ = 0; + capability_scope_correlation_failures_baseline_ = 0; } AmdProfilingSupport support; @@ -330,8 +337,9 @@ int RocprofilerBackend::toolInitialize() { void RocprofilerBackend::toolFinalize() { if (buffer_.handle != 0) { - (void) rocprofiler_flush_buffer(buffer_); - (void) rocprofiler_destroy_buffer(buffer_); + flushBuffers(); + (void) CheckStatus(rocprofiler_destroy_buffer(buffer_), + "rocprofiler_destroy_buffer"); } resetToolState(); @@ -349,11 +357,19 @@ void RocprofilerBackend::start() { void RocprofilerBackend::stop() { if (!active_.exchange(false) || context_.handle == 0) return; if (engine_) engine_->stop(); - (void) rocprofiler_stop_context(context_); + (void) CheckStatus(rocprofiler_stop_context(context_), + "rocprofiler_stop_context"); flushBuffers(); } void RocprofilerBackend::DrainProfilingData() { + // SegmentRuntime calls this immediately before choosing a segment + // boundary. Deliver the ROCprofiler activity buffer first so completed + // kernels and copies enter g_monitorBuffer while the retiring segment is + // still current; Monitor then drains that ring before publishing the next + // segment. Without this flush, a quiet workload can retain activity below + // the ROCprofiler watermark until a later segment or shutdown. + if (active_.load(std::memory_order_acquire)) flushBuffers(); if (engine_) engine_->drain(); } @@ -383,8 +399,14 @@ void RocprofilerBackend::emitCapabilities() { memcpy_rows_emitted_.load(std::memory_order_relaxed); const uint64_t dropped_records = trace_records_dropped_.load(std::memory_order_relaxed); + const uint64_t queue_dropped_records = + trace_records_queue_dropped_.load(std::memory_order_relaxed); + const uint64_t buffer_flush_failures = + trace_buffer_flush_failures_.load(std::memory_order_relaxed); const uint64_t unattributed_records = trace_records_unattributed_.load(std::memory_order_relaxed); + const uint64_t scope_correlation_failures = + scope_correlation_failures_.load(std::memory_order_relaxed); AmdCaptureCapabilityInput input; input.session_id = segment->session_id; @@ -400,8 +422,15 @@ void RocprofilerBackend::emitCapabilities() { engine_ && engine_->hasData() ? 1 : 0; input.dropped_trace_records = delta(dropped_records, capability_dropped_records_baseline_); + input.dropped_client_records = delta( + queue_dropped_records, capability_queue_dropped_records_baseline_); + input.trace_buffer_flush_failures = delta( + buffer_flush_failures, capability_buffer_flush_failures_baseline_); input.unattributed_trace_records = delta( unattributed_records, capability_unattributed_records_baseline_); + input.scope_correlation_failures = delta( + scope_correlation_failures, + capability_scope_correlation_failures_baseline_); segment->logger->write(model::CaptureCapabilitiesModel( BuildAmdCaptureCapabilitiesEvent(input))); @@ -409,7 +438,11 @@ void RocprofilerBackend::emitCapabilities() { capability_kernel_rows_baseline_ = kernel_rows; capability_memcpy_rows_baseline_ = memcpy_rows; capability_dropped_records_baseline_ = dropped_records; + capability_queue_dropped_records_baseline_ = queue_dropped_records; + capability_buffer_flush_failures_baseline_ = buffer_flush_failures; capability_unattributed_records_baseline_ = unattributed_records; + capability_scope_correlation_failures_baseline_ = + scope_correlation_failures; capture_capabilities_session_id_ = segment->session_id; } @@ -434,47 +467,96 @@ void RocprofilerBackend::shutdown() { } } -void RocprofilerBackend::flushBuffers() { - if (buffer_.handle != 0) (void) rocprofiler_flush_buffer(buffer_); +bool RocprofilerBackend::flushBuffers() { + if (buffer_.handle == 0) return true; + if (CheckStatus(rocprofiler_flush_buffer(buffer_), + "rocprofiler_flush_buffer")) { + return true; + } + trace_buffer_flush_failures_.fetch_add(1, std::memory_order_relaxed); + return false; } void RocprofilerBackend::OnScopeStart(const char* name) { if (!active_.load() || context_.handle == 0 || name == nullptr) return; rocprofiler_thread_id_t tid{}; - if (rocprofiler_get_thread_id(&tid) != ROCPROFILER_STATUS_SUCCESS) return; + if (!CheckStatus(rocprofiler_get_thread_id(&tid), + "rocprofiler_get_thread_id(scope start)")) { + scope_correlation_failures_.fetch_add(1, std::memory_order_relaxed); + return; + } + + // Thread-local scope state can outlive a backend lifecycle when a user + // shuts GPUFlight down inside an open scope. The metadata map is cleared + // with the ROCprofiler context, so a missing top ID identifies a stale + // stack and prevents it from contaminating the next session's scope path. + if (!g_scope_external_stack.empty()) { + std::lock_guard lock(external_scope_mutex_); + if (external_scope_metadata_.count(g_scope_external_stack.back()) == 0) { + g_scope_external_stack.clear(); + g_scope_name_stack.clear(); + } + } - static std::atomic next_scope_external{1}; const uint64_t external_value = - next_scope_external.fetch_add(1, std::memory_order_relaxed); + next_scope_external_.fetch_add(1, std::memory_order_relaxed); - g_scope_name_stack.emplace_back(name); std::string scope_path; - for (size_t i = 0; i < g_scope_name_stack.size(); ++i) { - if (i > 0) scope_path += "|"; - scope_path += g_scope_name_stack[i]; + for (const auto& component : g_scope_name_stack) { + if (!scope_path.empty()) scope_path += "|"; + scope_path += component; } + if (!scope_path.empty()) scope_path += "|"; + scope_path += name; + rocprofiler_user_data_t user_data{}; + user_data.value = external_value; + if (!CheckStatus(rocprofiler_push_external_correlation_id( + context_, tid, user_data), + "rocprofiler_push_external_correlation_id")) { + scope_correlation_failures_.fetch_add(1, std::memory_order_relaxed); + return; + } + + g_scope_name_stack.emplace_back(name); + g_scope_external_stack.push_back(external_value); { std::lock_guard lock(external_scope_mutex_); external_scope_metadata_[external_value] = ExternalScopeMetadata{ - scope_path, static_cast(g_scope_name_stack.size())}; + std::move(scope_path), static_cast(g_scope_name_stack.size())}; } - - g_scope_external_stack.push_back(external_value); - rocprofiler_user_data_t user_data{}; - user_data.value = external_value; - (void) rocprofiler_push_external_correlation_id(context_, tid, user_data); } -void RocprofilerBackend::OnScopeStop(const char*) { +void RocprofilerBackend::OnScopeStop(const char* name) { if (!active_.load() || context_.handle == 0) return; rocprofiler_thread_id_t tid{}; - if (rocprofiler_get_thread_id(&tid) != ROCPROFILER_STATUS_SUCCESS) return; + if (!CheckStatus(rocprofiler_get_thread_id(&tid), + "rocprofiler_get_thread_id(scope stop)")) { + scope_correlation_failures_.fetch_add(1, std::memory_order_relaxed); + return; + } rocprofiler_user_data_t user_data{}; - (void) rocprofiler_pop_external_correlation_id(context_, tid, &user_data); + if (!CheckStatus(rocprofiler_pop_external_correlation_id( + context_, tid, &user_data), + "rocprofiler_pop_external_correlation_id")) { + scope_correlation_failures_.fetch_add(1, std::memory_order_relaxed); + return; + } + + bool matched = !g_scope_external_stack.empty() && + g_scope_external_stack.back() == user_data.value; + if (name && !g_scope_name_stack.empty()) { + matched = matched && g_scope_name_stack.back() == name; + } + if (!matched) { + scope_correlation_failures_.fetch_add(1, std::memory_order_relaxed); + GFL_LOG_ERROR( + "[ROCProfilerBackend] scope correlation stack mismatch; " + "discarding the local top to preserve push/pop depth"); + } if (!g_scope_external_stack.empty()) g_scope_external_stack.pop_back(); if (!g_scope_name_stack.empty()) g_scope_name_stack.pop_back(); } @@ -873,6 +955,8 @@ void RocprofilerBackend::handleKernelDispatch( if (g_monitorBuffer.Push(out)) { kernel_rows_emitted_.fetch_add(1, std::memory_order_relaxed); + } else { + trace_records_queue_dropped_.fetch_add(1, std::memory_order_relaxed); } } @@ -919,6 +1003,8 @@ void RocprofilerBackend::handleMemoryCopy( if (g_monitorBuffer.Push(out)) { memcpy_rows_emitted_.fetch_add(1, std::memory_order_relaxed); + } else { + trace_records_queue_dropped_.fetch_add(1, std::memory_order_relaxed); } } diff --git a/include/gpufl/backends/amd/rocprofiler_backend.hpp b/include/gpufl/backends/amd/rocprofiler_backend.hpp index 80c9ee6..bd72fb1 100644 --- a/include/gpufl/backends/amd/rocprofiler_backend.hpp +++ b/include/gpufl/backends/amd/rocprofiler_backend.hpp @@ -44,7 +44,7 @@ class RocprofilerBackend final : public IMonitorBackend { void OnPerfScopeStart(const char* name) override; void OnPerfScopeStop(const char* name) override; - void flushBuffers(); + bool flushBuffers(); // Expose context and agent for engine initialization rocprofiler_context_id_t context() const { return context_; } @@ -149,13 +149,20 @@ class RocprofilerBackend final : public IMonitorBackend { std::atomic kernel_rows_emitted_{0}; std::atomic memcpy_rows_emitted_{0}; std::atomic trace_records_dropped_{0}; + std::atomic trace_records_queue_dropped_{0}; + std::atomic trace_buffer_flush_failures_{0}; std::atomic trace_records_unattributed_{0}; + std::atomic scope_correlation_failures_{0}; + std::atomic next_scope_external_{1}; mutable std::mutex capture_capabilities_mutex_; mutable std::string capture_capabilities_session_id_; mutable uint64_t capability_kernel_rows_baseline_ = 0; mutable uint64_t capability_memcpy_rows_baseline_ = 0; mutable uint64_t capability_dropped_records_baseline_ = 0; + mutable uint64_t capability_queue_dropped_records_baseline_ = 0; + mutable uint64_t capability_buffer_flush_failures_baseline_ = 0; mutable uint64_t capability_unattributed_records_baseline_ = 0; + mutable uint64_t capability_scope_correlation_failures_baseline_ = 0; std::atomic initialized_{false}; std::atomic active_{false}; diff --git a/tests/backends/amd/test_amd_profiling_policy.cpp b/tests/backends/amd/test_amd_profiling_policy.cpp index 1190f53..c2ef1d2 100644 --- a/tests/backends/amd/test_amd_profiling_policy.cpp +++ b/tests/backends/amd/test_amd_profiling_policy.cpp @@ -152,3 +152,55 @@ TEST(AmdCaptureCapabilities, DispatchSamplesAndDroppedTraceAreVisible) { EXPECT_NE(delivery->message.find("3 dropped trace record(s)"), std::string::npos); } + +TEST(AmdCaptureCapabilities, LifecycleDeliveryAndCorrelationFailuresAreVisible) { + gpufl::amd::AmdCaptureCapabilityInput input; + input.session_id = "amd-session"; + input.plan = gpufl::amd::ResolveAmdProfilingPlan( + gpufl::ProfilingEngine::Trace, {}); + input.trace_configured = true; + input.dropped_client_records = 5; + input.trace_buffer_flush_failures = 2; + input.scope_correlation_failures = 3; + + const auto event = gpufl::amd::BuildAmdCaptureCapabilitiesEvent(input); + + const auto* delivery = FindCapability(event, "trace_buffer_delivery"); + ASSERT_NE(delivery, nullptr); + EXPECT_EQ(delivery->status, "partial"); + EXPECT_EQ(delivery->reason_code, "rocprofiler_buffer_flush_failed"); + EXPECT_NE(delivery->message.find("failed 2 time(s)"), std::string::npos); + EXPECT_NE(delivery->message.find("wrong segment"), std::string::npos); + + const auto* correlation = FindCapability(event, "scope_correlation"); + ASSERT_NE(correlation, nullptr); + EXPECT_TRUE(correlation->requested); + EXPECT_EQ(correlation->status, "partial"); + EXPECT_EQ(correlation->mode, "rocprofiler_external_correlation"); + EXPECT_EQ(correlation->reason_code, + "rocprofiler_scope_correlation_failed"); + EXPECT_NE(correlation->message.find("failed 3 time(s)"), + std::string::npos); +} + +TEST(AmdCaptureCapabilities, ClientQueueDropsDegradeEndToEndDelivery) { + gpufl::amd::AmdCaptureCapabilityInput input; + input.session_id = "amd-session"; + input.plan = gpufl::amd::ResolveAmdProfilingPlan( + gpufl::ProfilingEngine::Trace, {}); + input.trace_configured = true; + input.dropped_client_records = 5; + + const auto event = gpufl::amd::BuildAmdCaptureCapabilitiesEvent(input); + + const auto* delivery = FindCapability(event, "trace_buffer_delivery"); + ASSERT_NE(delivery, nullptr); + EXPECT_EQ(delivery->status, "partial"); + EXPECT_EQ(delivery->reason_code, "gpufl_activity_queue_full"); + EXPECT_NE(delivery->message.find("dropped 5 trace record(s)"), + std::string::npos); + + const auto* correlation = FindCapability(event, "scope_correlation"); + ASSERT_NE(correlation, nullptr); + EXPECT_EQ(correlation->status, "enabled"); +} From 6f72b5e2d1a7984fbf26966d1c51f3adbe35ca71 Mon Sep 17 00:00:00 2001 From: Myoungho Shin Date: Thu, 27 Aug 2026 00:11:12 -0700 Subject: [PATCH 2/3] fix(amd): enforce deep window lifecycle --- .../amd/amd_dispatch_collection_gate.hpp | 53 +++++++++++++++++++ .../amd/engine/amd_profiling_engine.hpp | 7 +++ .../amd/engine/dispatch_counter_engine.cpp | 46 +++++++++++++--- .../amd/engine/dispatch_counter_engine.hpp | 12 ++++- .../backends/amd/rocprofiler_backend.cpp | 36 ++++++++++++- .../backends/amd/rocprofiler_backend.hpp | 5 ++ include/gpufl/core/deep_window.cpp | 36 +++++++++---- include/gpufl/core/deep_window.hpp | 7 ++- .../amd/test_amd_profiling_policy.cpp | 45 ++++++++++++++++ tests/core/test_deep_window.cpp | 4 +- 10 files changed, 228 insertions(+), 23 deletions(-) create mode 100644 include/gpufl/backends/amd/amd_dispatch_collection_gate.hpp diff --git a/include/gpufl/backends/amd/amd_dispatch_collection_gate.hpp b/include/gpufl/backends/amd/amd_dispatch_collection_gate.hpp new file mode 100644 index 0000000..94ad114 --- /dev/null +++ b/include/gpufl/backends/amd/amd_dispatch_collection_gate.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include "gpufl/core/monitor.hpp" + +namespace gpufl::amd { + +// Lock-free collection gate read from ROCprofiler's dispatch callback. In +// Always mode the running session supplies a counter profile for every +// dispatch. WindowOnly supplies no profile until a deep window opens, which +// ROCprofiler defines as "collect no counters for this dispatch." +class AmdDispatchCollectionGate { + public: + void configure(const DeepArmMode mode) { + window_only_ = mode == DeepArmMode::WindowOnly; + running_.store(false, std::memory_order_relaxed); + window_active_.store(false, std::memory_order_relaxed); + } + + void start() { running_.store(true, std::memory_order_release); } + + void stop() { + running_.store(false, std::memory_order_release); + window_active_.store(false, std::memory_order_release); + } + + void openWindow() { + window_active_.store(true, std::memory_order_release); + } + + void closeWindow() { + window_active_.store(false, std::memory_order_release); + } + + bool armed() const { + if (!running_.load(std::memory_order_acquire)) return false; + return !window_only_ || + window_active_.load(std::memory_order_acquire); + } + + bool collectDispatch(const bool window_claimed_launch) const { + if (!running_.load(std::memory_order_acquire)) return false; + return !window_only_ || window_claimed_launch; + } + + private: + bool window_only_ = false; + std::atomic running_{false}; + std::atomic window_active_{false}; +}; + +} // namespace gpufl::amd diff --git a/include/gpufl/backends/amd/engine/amd_profiling_engine.hpp b/include/gpufl/backends/amd/engine/amd_profiling_engine.hpp index d2699b2..297aa0c 100644 --- a/include/gpufl/backends/amd/engine/amd_profiling_engine.hpp +++ b/include/gpufl/backends/amd/engine/amd_profiling_engine.hpp @@ -37,6 +37,13 @@ class AmdProfilingEngine { /// True once this engine has emitted at least one profiling sample. virtual bool hasData() const = 0; + /// True once the context-bound service and its counter configuration are + /// ready for a deep window to arm. + virtual bool isPrepared() const = 0; + + /// Point-in-time state used by deep-window audit rows before disarming. + virtual bool isArmed() const = 0; + /// Scope hooks - engines may filter collection to scoped regions. virtual void onScopeStart(const char* /*name*/) {} virtual void onScopeStop(const char* /*name*/) {} diff --git a/include/gpufl/backends/amd/engine/dispatch_counter_engine.cpp b/include/gpufl/backends/amd/engine/dispatch_counter_engine.cpp index 22ded84..e091505 100644 --- a/include/gpufl/backends/amd/engine/dispatch_counter_engine.cpp +++ b/include/gpufl/backends/amd/engine/dispatch_counter_engine.cpp @@ -8,7 +8,10 @@ #include #include +#include "gpufl/core/common.hpp" #include "gpufl/core/debug_logger.hpp" +#include "gpufl/core/deep_window.hpp" +#include "gpufl/core/deep_window_rules.hpp" #include "gpufl/core/monitor.hpp" namespace gpufl::amd { @@ -54,8 +57,9 @@ bool CheckStatus(rocprofiler_status_t status, const char* call) { bool DispatchCounterEngine::initialize(const rocprofiler_context_id_t context, const rocprofiler_agent_id_t gpu_agent, - const MonitorOptions& /*opts*/) { + const MonitorOptions& opts) { context_ = context; + collection_gate_.configure(opts.deep_arm_mode); if (!discoverCounters(gpu_agent)) { GFL_LOG_ERROR("[DispatchCounterEngine] No counters discovered"); @@ -83,11 +87,14 @@ bool DispatchCounterEngine::initialize(const rocprofiler_context_id_t context, } void DispatchCounterEngine::start() { - // Context start is handled by the backend + // Context start is handled by the backend. The collection gate decides + // whether callbacks receive a profile immediately (Always) or only while + // a deep window is active (WindowOnly). + collection_gate_.start(); } void DispatchCounterEngine::stop() { - // Context stop is handled by the backend + collection_gate_.stop(); } void DispatchCounterEngine::drain() { @@ -95,12 +102,21 @@ void DispatchCounterEngine::drain() { } void DispatchCounterEngine::shutdown() { - if (config_valid_) { - rocprofiler_destroy_counter_config(config_id_); - config_valid_ = false; + collection_gate_.stop(); + if (config_valid_.exchange(false, std::memory_order_acq_rel)) { + (void) CheckStatus(rocprofiler_destroy_counter_config(config_id_), + "rocprofiler_destroy_counter_config"); } } +void DispatchCounterEngine::onScopeStart(const char*) { + collection_gate_.openWindow(); +} + +void DispatchCounterEngine::onScopeStop(const char*) { + collection_gate_.closeWindow(); +} + bool DispatchCounterEngine::discoverCounters(const rocprofiler_agent_id_t agent) { struct DiscoveryCtx { DispatchCounterEngine* engine; @@ -208,7 +224,7 @@ bool DispatchCounterEngine::createCounterConfig(const rocprofiler_agent_id_t age GFL_LOG_DEBUG("[DispatchCounterEngine] - ", name); } - config_valid_ = true; + config_valid_.store(true, std::memory_order_release); return true; } @@ -218,9 +234,23 @@ void DispatchCounterEngine::dispatchCallback( rocprofiler_user_data_t* /*user_data*/, void* callback_data) { auto* engine = static_cast(callback_data); - if (engine && engine->config_valid_ && config) { + if (!engine) return; + + // ROCprofiler treats a callback that supplies no profile as an explicit + // "collect no counters for this dispatch" decision. Clear the output so + // WindowOnly stays cheap outside the window. Claim the budget first: the + // Nth dispatch still receives the profile, while later callbacks reject + // collection immediately without running teardown on this callback path. + const bool window_claimed_launch = DeepWindow::OnLaunch(); + if (config) *config = {}; + if (config && + engine->collection_gate_.collectDispatch(window_claimed_launch)) { *config = engine->config_id_; } + + if (detail::DeepWindowRules::WantsLaunchFeed()) { + detail::DeepWindowRules::NoteKernelLaunch(detail::GetTimestampNs()); + } } void DispatchCounterEngine::recordCallback( diff --git a/include/gpufl/backends/amd/engine/dispatch_counter_engine.hpp b/include/gpufl/backends/amd/engine/dispatch_counter_engine.hpp index 27d184a..9959c62 100644 --- a/include/gpufl/backends/amd/engine/dispatch_counter_engine.hpp +++ b/include/gpufl/backends/amd/engine/dispatch_counter_engine.hpp @@ -10,6 +10,7 @@ #include #include +#include "gpufl/backends/amd/amd_dispatch_collection_gate.hpp" #include "gpufl/backends/amd/engine/amd_profiling_engine.hpp" namespace gpufl::amd { @@ -30,6 +31,14 @@ class DispatchCounterEngine final : public AmdProfilingEngine { void drain() override; void shutdown() override; bool hasData() const override { return sample_count_.load() > 0; } + bool isPrepared() const override { + return config_valid_.load(std::memory_order_acquire); + } + bool isArmed() const override { + return isPrepared() && collection_gate_.armed(); + } + void onScopeStart(const char* name) override; + void onScopeStop(const char* name) override; private: /// Counter metadata for resolving record IDs to human-readable names. @@ -57,7 +66,8 @@ class DispatchCounterEngine final : public AmdProfilingEngine { rocprofiler_context_id_t context_{}; rocprofiler_counter_config_id_t config_id_{}; - bool config_valid_ = false; + std::atomic config_valid_{false}; + AmdDispatchCollectionGate collection_gate_; mutable std::mutex counter_mu_; std::unordered_map counter_info_; // counter_id.handle → info diff --git a/include/gpufl/backends/amd/rocprofiler_backend.cpp b/include/gpufl/backends/amd/rocprofiler_backend.cpp index 310e387..87546f7 100644 --- a/include/gpufl/backends/amd/rocprofiler_backend.cpp +++ b/include/gpufl/backends/amd/rocprofiler_backend.cpp @@ -28,6 +28,7 @@ #include "gpufl/backends/amd/amd_capture_capabilities.hpp" #include "gpufl/core/common.hpp" #include "gpufl/core/debug_logger.hpp" +#include "gpufl/core/deep_window.hpp" #include "gpufl/core/monitor.hpp" #include "gpufl/core/logger/logger.hpp" #include "gpufl/core/model/lifecycle_model.hpp" @@ -373,11 +374,44 @@ void RocprofilerBackend::DrainProfilingData() { if (engine_) engine_->drain(); } +void RocprofilerBackend::ServiceDeepWindow() { + if (!initialized_.load(std::memory_order_acquire) || + !active_.load(std::memory_order_acquire) || + !DeepWindow::HasPendingWork()) { + return; + } + DeepWindow::ServicePending(); +} + +bool RocprofilerBackend::DeepEnginesPrepared() const { + return engine_ != nullptr && engine_->isPrepared(); +} + +std::vector RocprofilerBackend::OnDeepWindowStop( + const char* name) { + std::vector armed; + if (engine_ && engine_->isArmed()) { + armed.emplace_back(SelectedEngineWireName()); + } + OnScopeStop(name); + return armed; +} + void RocprofilerBackend::OnPerfScopeStart(const char* name) { - if (engine_) engine_->onScopeStart(name); + if (opts_.deep_arm_mode == DeepArmMode::WindowOnly) return; + OnDeepWindowPerfStart(name); } void RocprofilerBackend::OnPerfScopeStop(const char* name) { + if (opts_.deep_arm_mode == DeepArmMode::WindowOnly) return; + OnDeepWindowPerfStop(name); +} + +void RocprofilerBackend::OnDeepWindowPerfStart(const char* name) { + if (engine_) engine_->onScopeStart(name); +} + +void RocprofilerBackend::OnDeepWindowPerfStop(const char* name) { if (engine_) engine_->onScopeStop(name); } diff --git a/include/gpufl/backends/amd/rocprofiler_backend.hpp b/include/gpufl/backends/amd/rocprofiler_backend.hpp index bd72fb1..82d2be7 100644 --- a/include/gpufl/backends/amd/rocprofiler_backend.hpp +++ b/include/gpufl/backends/amd/rocprofiler_backend.hpp @@ -41,8 +41,13 @@ class RocprofilerBackend final : public IMonitorBackend { void OnScopeStart(const char* name) override; void OnScopeStop(const char* name) override; void DrainProfilingData() override; + void ServiceDeepWindow() override; + bool DeepEnginesPrepared() const override; + std::vector OnDeepWindowStop(const char* name) override; void OnPerfScopeStart(const char* name) override; void OnPerfScopeStop(const char* name) override; + void OnDeepWindowPerfStart(const char* name) override; + void OnDeepWindowPerfStop(const char* name) override; bool flushBuffers(); diff --git a/include/gpufl/core/deep_window.cpp b/include/gpufl/core/deep_window.cpp index 5cea021..8c3ee27 100644 --- a/include/gpufl/core/deep_window.cpp +++ b/include/gpufl/core/deep_window.cpp @@ -31,7 +31,10 @@ std::mutex g_mu; std::atomic g_active{false}; std::atomic g_deadline_ns{0}; // 0 = no time bound -std::atomic g_launches_remaining{0}; // 0 = no launch bound +// Separating "bounded" from remaining=0 distinguishes an unlimited window +// from a bounded window whose final slot was already claimed. +std::atomic g_launch_bound_enabled{false}; +std::atomic g_launches_remaining{0}; std::atomic g_launches_covered{0}; // Set by the launch callback when a bound is reached; consumed by the // collector, which is the thread allowed to run the engines' teardown. @@ -242,6 +245,8 @@ bool DeepWindow::Open(const DeepWindowSpec& spec) { : 0, std::memory_order_relaxed); g_launches_remaining.store(spec.max_launches, std::memory_order_relaxed); + g_launch_bound_enabled.store(spec.max_launches > 0, + std::memory_order_relaxed); g_launches_covered.store(0, std::memory_order_relaxed); g_close_requested.store(false, std::memory_order_relaxed); name = g_name; @@ -514,20 +519,30 @@ void DeepWindow::TakePendingOpen_() { g_claimed_token = 0; } -void DeepWindow::OnLaunch() { +bool DeepWindow::OnLaunch() { // Arming is the collector's job too - see ServicePending. This callback // only counts. - if (!g_active.load(std::memory_order_acquire)) return; - - g_launches_covered.fetch_add(1, std::memory_order_relaxed); + if (!g_active.load(std::memory_order_acquire)) return false; - if (g_launches_remaining.load(std::memory_order_relaxed) > 0) { - // fetch_sub returns the PREVIOUS value, so 1 means this launch - // consumed the last of the budget. - if (g_launches_remaining.fetch_sub(1, std::memory_order_relaxed) <= 1) { - RequestClose_(DeepWindowClose::LaunchBudget); + if (g_launch_bound_enabled.load(std::memory_order_relaxed)) { + uint64_t remaining = + g_launches_remaining.load(std::memory_order_relaxed); + while (remaining > 0) { + if (g_launches_remaining.compare_exchange_weak( + remaining, remaining - 1, std::memory_order_acq_rel, + std::memory_order_relaxed)) { + g_launches_covered.fetch_add(1, std::memory_order_relaxed); + if (remaining == 1) { + RequestClose_(DeepWindowClose::LaunchBudget); + } + return true; + } } + return false; } + + g_launches_covered.fetch_add(1, std::memory_order_relaxed); + return true; } void DeepWindow::RequestClose_(const DeepWindowClose reason) { @@ -583,6 +598,7 @@ void DeepWindow::ResetForTesting() { g_pending = PendingOpen{}; g_deadline_ns.store(0, std::memory_order_relaxed); g_launches_remaining.store(0, std::memory_order_relaxed); + g_launch_bound_enabled.store(false, std::memory_order_relaxed); g_launches_covered.store(0, std::memory_order_relaxed); g_close_requested.store(false, std::memory_order_relaxed); g_close_reason.store(static_cast(DeepWindowClose::Deadline), diff --git a/include/gpufl/core/deep_window.hpp b/include/gpufl/core/deep_window.hpp index b452362..b4b3512 100644 --- a/include/gpufl/core/deep_window.hpp +++ b/include/gpufl/core/deep_window.hpp @@ -175,14 +175,17 @@ class DeepWindow { * @brief Per-launch bound accounting, driven from the CUPTI launch * callback. * - * Consumes one launch of budget and RECORDS that a bound was reached. + * Claims one launch of budget and RECORDS that a bound was reached. * It deliberately does not close: this runs inside a CUPTI callback, * and the engines' teardown calls (cuptiPmSamplingDecodeData, * cuptiPCSamplingStop) return CUPTI_ERROR_UNKNOWN when invoked from * there. Verified on Linux/driver 610.43; Windows happened to tolerate * it, which is why the first version looked correct. + * + * @return True when this launch claimed a slot in the active window; + * false when no window is active or its budget is already spent. */ - static void OnLaunch(); + static bool OnLaunch(); /** * @brief Cheap, lock-free: is there an arm or a disarm waiting? diff --git a/tests/backends/amd/test_amd_profiling_policy.cpp b/tests/backends/amd/test_amd_profiling_policy.cpp index c2ef1d2..92a2e77 100644 --- a/tests/backends/amd/test_amd_profiling_policy.cpp +++ b/tests/backends/amd/test_amd_profiling_policy.cpp @@ -3,6 +3,7 @@ #include #include "gpufl/backends/amd/amd_capture_capabilities.hpp" +#include "gpufl/backends/amd/amd_dispatch_collection_gate.hpp" #include "gpufl/backends/amd/amd_profiling_policy.hpp" namespace { @@ -18,6 +19,50 @@ const gpufl::CaptureCapability* FindCapability( } // namespace +TEST(AmdDispatchCollectionGate, AlwaysModeFollowsSessionLifetime) { + gpufl::amd::AmdDispatchCollectionGate gate; + gate.configure(gpufl::DeepArmMode::Always); + + EXPECT_FALSE(gate.armed()); + gate.start(); + EXPECT_TRUE(gate.armed()); + EXPECT_TRUE(gate.collectDispatch(false)); + gate.closeWindow(); + EXPECT_TRUE(gate.armed()); + gate.stop(); + EXPECT_FALSE(gate.armed()); +} + +TEST(AmdDispatchCollectionGate, WindowOnlyArmsExactlyInsideWindow) { + gpufl::amd::AmdDispatchCollectionGate gate; + gate.configure(gpufl::DeepArmMode::WindowOnly); + + gate.start(); + EXPECT_FALSE(gate.armed()); + gate.openWindow(); + EXPECT_TRUE(gate.armed()); + EXPECT_TRUE(gate.collectDispatch(true)); + EXPECT_FALSE(gate.collectDispatch(false)); + gate.closeWindow(); + EXPECT_FALSE(gate.armed()); + // A callback that claimed the final slot before collector-thread disarm + // still owns that dispatch. + EXPECT_TRUE(gate.collectDispatch(true)); + EXPECT_FALSE(gate.collectDispatch(false)); +} + +TEST(AmdDispatchCollectionGate, StopClearsWindowBeforeRestart) { + gpufl::amd::AmdDispatchCollectionGate gate; + gate.configure(gpufl::DeepArmMode::WindowOnly); + + gate.openWindow(); + gate.start(); + ASSERT_TRUE(gate.armed()); + gate.stop(); + gate.start(); + EXPECT_FALSE(gate.armed()); +} + TEST(AmdProfilingPolicy, RequestIntentNeverInventsAmdNativeNames) { EXPECT_STREQ(gpufl::amd::AmdRequestIntentWireName( gpufl::ProfilingEngine::PcSampling), diff --git a/tests/core/test_deep_window.cpp b/tests/core/test_deep_window.cpp index b01d8d0..34347db 100644 --- a/tests/core/test_deep_window.cpp +++ b/tests/core/test_deep_window.cpp @@ -212,7 +212,9 @@ TEST_F(DeepWindowTest, OnLaunchWithNoWindowOpenIsHarmless) { TEST_F(DeepWindowTest, LaunchAloneNeverClosesTheWindow) { ASSERT_TRUE(gpufl::DeepWindow::Open(Spec(0, /*launches=*/1))); - gpufl::DeepWindow::OnLaunch(); // budget spent, but no teardown here + EXPECT_TRUE(gpufl::DeepWindow::OnLaunch()); + EXPECT_FALSE(gpufl::DeepWindow::OnLaunch()) + << "a later launch must not exceed the claimed budget"; EXPECT_TRUE(gpufl::DeepWindow::Active()) << "the launch callback must not run the teardown"; From 30a963bda86cbf4fbd28bc95ba0ab05cfefbaed7 Mon Sep 17 00:00:00 2001 From: Myoungho Shin Date: Thu, 27 Aug 2026 22:33:53 -0700 Subject: [PATCH 3/3] fix(amd): preserve dispatch device attribution --- .../backends/amd/amd_profiling_policy.cpp | 11 ++++++++++ .../backends/amd/amd_profiling_policy.hpp | 10 ++++++++++ .../amd/engine/amd_profiling_engine.hpp | 1 + .../amd/engine/dispatch_counter_engine.cpp | 20 ++++++++++++++----- .../amd/engine/dispatch_counter_engine.hpp | 3 +++ .../backends/amd/rocprofiler_backend.cpp | 16 +++++++++++++-- .../amd/test_amd_profiling_policy.cpp | 13 ++++++++++++ 7 files changed, 67 insertions(+), 7 deletions(-) diff --git a/include/gpufl/backends/amd/amd_profiling_policy.cpp b/include/gpufl/backends/amd/amd_profiling_policy.cpp index c1146d2..8f0a32a 100644 --- a/include/gpufl/backends/amd/amd_profiling_policy.cpp +++ b/include/gpufl/backends/amd/amd_profiling_policy.cpp @@ -60,6 +60,17 @@ bool AmdRequestNeedsDeviceCounting(const ProfilingEngine engine) { engine == ProfilingEngine::Deep; } +std::optional ResolveAmdDispatchDeviceId( + const uint64_t configured_agent_handle, + const uint32_t configured_device_id, + const uint64_t dispatch_agent_handle) { + if (configured_agent_handle == 0 || dispatch_agent_handle == 0 || + configured_agent_handle != dispatch_agent_handle) { + return std::nullopt; + } + return configured_device_id; +} + AmdResolvedProfilingPlan ResolveAmdProfilingPlan( const ProfilingEngine requested, const AmdProfilingSupport& support) { diff --git a/include/gpufl/backends/amd/amd_profiling_policy.hpp b/include/gpufl/backends/amd/amd_profiling_policy.hpp index 1035fb5..dd8d321 100644 --- a/include/gpufl/backends/amd/amd_profiling_policy.hpp +++ b/include/gpufl/backends/amd/amd_profiling_policy.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include "gpufl/core/monitor.hpp" @@ -41,4 +43,12 @@ bool AmdRequestNeedsDispatchCounting(ProfilingEngine engine); bool AmdRequestNeedsPcSampling(ProfilingEngine engine); bool AmdRequestNeedsDeviceCounting(ProfilingEngine engine); +// Dispatch counting is currently configured for one GPU agent. Resolve only +// records from that agent so a secondary GPU can never be mislabeled as device +// zero (or as the configured primary device). +std::optional ResolveAmdDispatchDeviceId( + uint64_t configured_agent_handle, + uint32_t configured_device_id, + uint64_t dispatch_agent_handle); + } // namespace gpufl::amd diff --git a/include/gpufl/backends/amd/engine/amd_profiling_engine.hpp b/include/gpufl/backends/amd/engine/amd_profiling_engine.hpp index 297aa0c..ea6bcbe 100644 --- a/include/gpufl/backends/amd/engine/amd_profiling_engine.hpp +++ b/include/gpufl/backends/amd/engine/amd_profiling_engine.hpp @@ -20,6 +20,7 @@ class AmdProfilingEngine { /// Returns false if the hardware/driver doesn't support this engine. virtual bool initialize(rocprofiler_context_id_t context, rocprofiler_agent_id_t gpu_agent, + uint32_t gpu_device_id, const MonitorOptions& opts) = 0; /// Begin profiling (context is already started). diff --git a/include/gpufl/backends/amd/engine/dispatch_counter_engine.cpp b/include/gpufl/backends/amd/engine/dispatch_counter_engine.cpp index e091505..1b3fc8f 100644 --- a/include/gpufl/backends/amd/engine/dispatch_counter_engine.cpp +++ b/include/gpufl/backends/amd/engine/dispatch_counter_engine.cpp @@ -8,6 +8,8 @@ #include #include +#include "gpufl/backends/amd/amd_profiling_policy.hpp" + #include "gpufl/core/common.hpp" #include "gpufl/core/debug_logger.hpp" #include "gpufl/core/deep_window.hpp" @@ -57,8 +59,11 @@ bool CheckStatus(rocprofiler_status_t status, const char* call) { bool DispatchCounterEngine::initialize(const rocprofiler_context_id_t context, const rocprofiler_agent_id_t gpu_agent, + const uint32_t gpu_device_id, const MonitorOptions& opts) { context_ = context; + gpu_agent_ = gpu_agent; + gpu_device_id_ = gpu_device_id; collection_gate_.configure(opts.deep_arm_mode); if (!discoverCounters(gpu_agent)) { @@ -229,7 +234,7 @@ bool DispatchCounterEngine::createCounterConfig(const rocprofiler_agent_id_t age } void DispatchCounterEngine::dispatchCallback( - rocprofiler_dispatch_counting_service_data_t /*dispatch_data*/, + rocprofiler_dispatch_counting_service_data_t dispatch_data, rocprofiler_counter_config_id_t* config, rocprofiler_user_data_t* /*user_data*/, void* callback_data) { @@ -242,8 +247,11 @@ void DispatchCounterEngine::dispatchCallback( // Nth dispatch still receives the profile, while later callbacks reject // collection immediately without running teardown on this callback path. const bool window_claimed_launch = DeepWindow::OnLaunch(); + const auto device_id = ResolveAmdDispatchDeviceId( + engine->gpu_agent_.handle, engine->gpu_device_id_, + dispatch_data.dispatch_info.agent_id.handle); if (config) *config = {}; - if (config && + if (config && device_id.has_value() && engine->collection_gate_.collectDispatch(window_claimed_launch)) { *config = engine->config_id_; } @@ -262,8 +270,10 @@ void DispatchCounterEngine::recordCallback( auto* engine = static_cast(callback_data); if (!engine || !record_data || record_count == 0) return; - const auto& info = dispatch_data.dispatch_info; - (void)info; // reserved for future agent_id → device_id resolution + const auto device_id = ResolveAmdDispatchDeviceId( + engine->gpu_agent_.handle, engine->gpu_device_id_, + dispatch_data.dispatch_info.agent_id.handle); + if (!device_id.has_value()) return; const auto corr_id = dispatch_data.correlation_id.internal; const int64_t now_ns = static_cast(dispatch_data.start_timestamp); @@ -298,7 +308,7 @@ void DispatchCounterEngine::recordCallback( ProfileSampleInput s; s.ts_ns = now_ns; s.corr_id = static_cast(corr_id & 0xFFFFFFFF); - s.device_id = 0; // TODO: resolve from agent_id + s.device_id = *device_id; s.sample_kind = 1; // sass_metric s.metric_name = counter_name; s.metric_value = static_cast(rec.counter_value); diff --git a/include/gpufl/backends/amd/engine/dispatch_counter_engine.hpp b/include/gpufl/backends/amd/engine/dispatch_counter_engine.hpp index 9959c62..2a58d92 100644 --- a/include/gpufl/backends/amd/engine/dispatch_counter_engine.hpp +++ b/include/gpufl/backends/amd/engine/dispatch_counter_engine.hpp @@ -25,6 +25,7 @@ class DispatchCounterEngine final : public AmdProfilingEngine { bool initialize(rocprofiler_context_id_t context, rocprofiler_agent_id_t gpu_agent, + uint32_t gpu_device_id, const MonitorOptions& opts) override; void start() override; void stop() override; @@ -65,6 +66,8 @@ class DispatchCounterEngine final : public AmdProfilingEngine { void* callback_data); rocprofiler_context_id_t context_{}; + rocprofiler_agent_id_t gpu_agent_{}; + uint32_t gpu_device_id_ = 0; rocprofiler_counter_config_id_t config_id_{}; std::atomic config_valid_{false}; AmdDispatchCollectionGate collection_gate_; diff --git a/include/gpufl/backends/amd/rocprofiler_backend.cpp b/include/gpufl/backends/amd/rocprofiler_backend.cpp index 87546f7..b6de107 100644 --- a/include/gpufl/backends/amd/rocprofiler_backend.cpp +++ b/include/gpufl/backends/amd/rocprofiler_backend.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -307,8 +308,18 @@ int RocprofilerBackend::toolInitialize() { // Resolve the user-facing request to an AMD-native path before creating an // engine. Unsupported requests remain trace-capable and are reported as // explicit fallbacks rather than silently pretending the requested engine ran. + std::optional primary_device_id; + { + std::lock_guard lock(agent_mutex_); + if (const auto it = gpu_device_ids_.find(primary_gpu_agent_.handle); + it != gpu_device_ids_.end()) { + primary_device_id = static_cast(it->second); + } + } + AmdProfilingSupport support; - support.dispatch_counting = primary_gpu_agent_.handle != 0; + support.dispatch_counting = primary_gpu_agent_.handle != 0 && + primary_device_id.has_value(); auto plan = ResolveAmdProfilingPlan(opts_.profiling_engine, support); setResolvedPlan(plan); @@ -321,7 +332,8 @@ int RocprofilerBackend::toolInitialize() { if (plan.selected_path == AmdProfilingPath::DispatchCounting) { engine_ = std::make_unique(); - if (!engine_->initialize(context_, primary_gpu_agent_, opts_)) { + if (!engine_->initialize(context_, primary_gpu_agent_, + *primary_device_id, opts_)) { GFL_LOG_ERROR( "[ROCProfilerBackend] Dispatch-counter initialization failed; " "continuing with ROCprofiler trace activity only"); diff --git a/tests/backends/amd/test_amd_profiling_policy.cpp b/tests/backends/amd/test_amd_profiling_policy.cpp index 92a2e77..11c8ca8 100644 --- a/tests/backends/amd/test_amd_profiling_policy.cpp +++ b/tests/backends/amd/test_amd_profiling_policy.cpp @@ -138,6 +138,19 @@ TEST(AmdProfilingPolicy, DeepReportsDispatchOnlyPartialImplementation) { EXPECT_EQ(plan.reason_code, "deep_services_unavailable_dispatch_counting_selected"); } +TEST(AmdProfilingPolicy, DispatchSamplesKeepConfiguredDeviceIdentity) { + EXPECT_EQ(gpufl::amd::ResolveAmdDispatchDeviceId(101, 7, 101), 7u); +} + +TEST(AmdProfilingPolicy, DispatchSamplesRejectOtherOrUnknownAgents) { + EXPECT_FALSE( + gpufl::amd::ResolveAmdDispatchDeviceId(101, 7, 202).has_value()); + EXPECT_FALSE( + gpufl::amd::ResolveAmdDispatchDeviceId(0, 7, 101).has_value()); + EXPECT_FALSE( + gpufl::amd::ResolveAmdDispatchDeviceId(101, 7, 0).has_value()); +} + TEST(AmdCaptureCapabilities, PcFallbackNamesRequestAndSelectionSeparately) { gpufl::amd::AmdCaptureCapabilityInput input; input.session_id = "amd-session";