Files
TheRock/rocm-systems/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-attach/queue_registration.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

465 lines
18 KiB
C++

// MIT License
//
// Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#include "queue_registration.h"
#include "queue_registration.hpp"
#include "table.hpp"
#include "lib/common/static_object.hpp"
#include <mutex>
namespace
{
using callback_t = void (*)(hsa_status_t status, hsa_queue_t* source, void* data);
struct queue_entry_t
{
hsa_agent_t agent = hsa_agent_t{};
write_interceptor_t user_write_interceptor_func = nullptr;
void* user_write_interceptor_data = nullptr;
};
struct queue_cb_entry_t
{
rocprofiler_attach_queue_cb_t cb = nullptr;
void* data = nullptr;
};
using queue_collection_t = std::unordered_map<hsa_queue_t*, queue_entry_t>;
struct queue_registration_t
{
// guards access to both queues and cb_list
std::mutex mutex;
queue_collection_t queues;
std::vector<queue_cb_entry_t> cb_list;
decltype(AmdExtTable::hsa_amd_queue_intercept_create_fn) hsa_amd_queue_intercept_create_fn =
nullptr;
decltype(AmdExtTable::hsa_amd_profiling_set_profiler_enabled_fn)
hsa_amd_profiling_set_profiler_enabled_fn = nullptr;
decltype(AmdExtTable::hsa_amd_queue_intercept_register_fn) hsa_amd_queue_intercept_register_fn =
nullptr;
#if defined(HSA_AMD_EXT_API_TABLE_STEP_VERSION) && HSA_AMD_EXT_API_TABLE_STEP_VERSION >= 0x10
// Real ROCr hsa_amd_queue_create, used to forward non-compute (e.g. SDMA) queue requests that
// cannot be wrapped in an InterceptQueue.
decltype(AmdExtTable::hsa_amd_queue_create_fn) hsa_amd_queue_create_fn = nullptr;
#endif
decltype(CoreApiTable::hsa_status_string_fn) hsa_status_string_fn = nullptr;
};
queue_registration_t*
get_queue_registration()
{
static auto*& registration =
rocprofiler::common::static_object<queue_registration_t>::construct();
return registration;
}
std::string_view
get_hsa_status_string(hsa_status_t _status)
{
auto* registration = CHECK_NOTNULL(get_queue_registration());
const char* _status_msg = nullptr;
return (CHECK_NOTNULL(registration->hsa_status_string_fn)(_status, &_status_msg) ==
HSA_STATUS_SUCCESS &&
_status_msg)
? std::string_view{_status_msg}
: std::string_view{"(unknown HSA error)"};
}
#define ROCP_ATTACH_HSA_TABLE_CALL(SEVERITY, EXPR) \
auto ROCPROFILER_VARIABLE(rocp_hsa_table_call_, __LINE__) = (EXPR); \
ROCP_##SEVERITY##_IF(ROCPROFILER_VARIABLE(rocp_hsa_table_call_, __LINE__) != \
HSA_STATUS_SUCCESS) \
<< #EXPR << " returned non-zero status code " \
<< ROCPROFILER_VARIABLE(rocp_hsa_table_call_, __LINE__) \
<< " :: " << get_hsa_status_string(ROCPROFILER_VARIABLE(rocp_hsa_table_call_, __LINE__)) \
<< " "
// This is the attach library's WriteInterceptor that is provided to HSA.
// Since the interceptor function cannot be changed later, this shim is provided immediately upon
// queue creation. This shim's user data is a reference to the queue_entry_t for this queue, which
// will then by cast and used to call the user write interceptor if it is non-null.
void
write_interceptor(const void* packets,
uint64_t pkt_count,
uint64_t unused,
void* data,
hsa_amd_queue_intercept_packet_writer_t writer)
{
ROCP_FATAL_IF(data == nullptr) << "WriteInterceptor was not passed a valid pointer";
auto* registration = CHECK_NOTNULL(get_queue_registration());
const auto* entry = static_cast<const queue_entry_t*>(data);
write_interceptor_t func = nullptr;
void* udata = nullptr;
{
std::lock_guard lg(registration->mutex);
func = entry->user_write_interceptor_func;
udata = entry->user_write_interceptor_data;
}
if(func)
{
func(packets, pkt_count, unused, udata, writer);
}
else
{
writer(packets, pkt_count);
}
}
// Shared finalization for a freshly created InterceptQueue: enable profiling, register the write
// interceptor shim, record the queue, and notify queue-created callbacks. Used by both the classic
// (hsa_queue_create) and descriptor-based (hsa_amd_queue_create) intercept paths.
hsa_status_t
finalize_intercept_queue(hsa_agent_t agent, hsa_queue_t* new_queue)
{
auto* registration = CHECK_NOTNULL(get_queue_registration());
ROCP_ATTACH_HSA_TABLE_CALL(
FATAL, registration->hsa_amd_profiling_set_profiler_enabled_fn(new_queue, true))
<< "Could not setup intercept profiler";
// Create and insert our queue's data entry now, as we need to provide a reference to it for the
// write_interceptor
queue_entry_t entry{};
entry.agent = agent;
queue_entry_t* write_interceptor_data = nullptr;
auto snapshot = std::vector<queue_cb_entry_t>{};
{
std::lock_guard lg(registration->mutex);
ROCP_FATAL_IF(registration->queues.count(new_queue) > 0)
<< "Queue registration already contains an entry for new queue handle " << new_queue;
registration->queues.insert({new_queue, entry});
write_interceptor_data = &(registration->queues.at(new_queue));
snapshot = std::vector<queue_cb_entry_t>{registration->cb_list};
}
// Pass queue_entry_t* as user data, used to directly call the user's write interceptor
ROCP_ATTACH_HSA_TABLE_CALL(FATAL,
registration->hsa_amd_queue_intercept_register_fn(
new_queue, write_interceptor, write_interceptor_data))
<< "Could not register interceptor";
ROCP_INFO << "created attach queue for HSA agent handle " << agent.handle;
for(auto& cb_entry : snapshot)
{
if(cb_entry.cb)
{
cb_entry.cb(new_queue, agent, ROCPROFILER_ATTACH_QUEUE_CREATED, cb_entry.data);
}
}
return HSA_STATUS_SUCCESS;
}
// HSA Intercept Functions (create_queue/destroy_queue)
hsa_status_t
create_queue(hsa_agent_t agent,
uint32_t size,
hsa_queue_type32_t type,
callback_t callback,
void* data,
uint32_t private_segment_size,
uint32_t group_segment_size,
hsa_queue_t** queue)
{
auto* registration = CHECK_NOTNULL(get_queue_registration());
// Create new queue in HSA
hsa_queue_t* new_queue = nullptr;
ROCP_FATAL_IF(!registration->hsa_amd_queue_intercept_create_fn ||
!registration->hsa_amd_profiling_set_profiler_enabled_fn ||
!registration->hsa_amd_queue_intercept_register_fn ||
!registration->hsa_status_string_fn)
<< "Queue registration was not initialized before create queue was called!";
ROCP_ATTACH_HSA_TABLE_CALL(FATAL,
registration->hsa_amd_queue_intercept_create_fn(agent,
size,
type,
callback,
data,
private_segment_size,
group_segment_size,
&new_queue))
<< "Could not create intercept queue";
auto status = finalize_intercept_queue(agent, new_queue);
if(status != HSA_STATUS_SUCCESS) return status;
*queue = new_queue;
return HSA_STATUS_SUCCESS;
}
#if defined(HSA_AMD_EXT_API_TABLE_STEP_VERSION) && HSA_AMD_EXT_API_TABLE_STEP_VERSION >= 0x10
// Descriptor-based queue creation (hsa_amd_queue_create). A queue created by this API is a plain
// queue that cannot carry a ROCr packet interceptor, so for compute queues we build an
// InterceptQueue that mirrors the descriptor's compute parameters instead (the InterceptQueue is
// what allows kernel-dispatch tracing under attachment). Non-compute queues (e.g. SDMA) do not
// dispatch kernels and are forwarded to the real implementation unchanged.
//
// NOTE: the InterceptQueue is created via the classic hsa_queue_create path, so the descriptor's
// device-memory ring-buffer flag is not honored while the queue is being profiled; the queue falls
// back to a system-memory ring.
//
// KNOWN LIMITATION (late attach): This wrapper bypasses the real hsa_amd_queue_create
// implementation for compute descriptors, so the runtime's descriptor validation (version,
// reserved fields, size, priority, CU-mask constraints) and partial-batch-success semantics are
// not applied. Late attach is excluded from the supported configuration for this PR; a
// descriptor-aware attach implementation that delegates to the real runtime for validation and
// then intercepts the resulting queue is needed before late attach can be re-enabled with the new
// queue creation API.
hsa_status_t
create_amd_queue(hsa_agent_t agent, hsa_amd_queue_create_desc_t* descs, uint32_t num_descs)
{
auto* registration = CHECK_NOTNULL(get_queue_registration());
ROCP_FATAL_IF(!registration->hsa_amd_queue_intercept_create_fn ||
!registration->hsa_amd_profiling_set_profiler_enabled_fn ||
!registration->hsa_amd_queue_intercept_register_fn ||
!registration->hsa_amd_queue_create_fn || !registration->hsa_status_string_fn)
<< "Queue registration was not initialized before create queue was called!";
for(uint32_t desc_idx = 0; desc_idx < num_descs; ++desc_idx)
{
if(descs[desc_idx].engine_type != HSA_AMD_QUEUE_ENGINE_COMPUTE)
{
// Non-compute queues cannot dispatch kernels; forward to the real implementation so the
// queue is still created (just not intercepted).
ROCP_ATTACH_HSA_TABLE_CALL(
FATAL, registration->hsa_amd_queue_create_fn(agent, &descs[desc_idx], 1))
<< "Could not create non-compute amd queue";
continue;
}
const auto& compute_params = descs[desc_idx].engine.compute;
const uint32_t ring_packets = static_cast<uint32_t>(descs[desc_idx].queue_size_bytes /
sizeof(hsa_kernel_dispatch_packet_t));
hsa_queue_t* new_queue = nullptr;
ROCP_ATTACH_HSA_TABLE_CALL(
FATAL,
registration->hsa_amd_queue_intercept_create_fn(
agent,
ring_packets,
compute_params.type,
descs[desc_idx].callback,
descs[desc_idx].callback_data,
compute_params.private_segment_size,
// Descriptor v1 does not expose group_segment_size; UINT32_MAX requests the runtime
// default, matching the classic hsa_queue_create behavior.
UINT32_MAX,
&new_queue))
<< "Could not create intercept queue";
auto status = finalize_intercept_queue(agent, new_queue);
if(status != HSA_STATUS_SUCCESS) return status;
descs[desc_idx].queue = new_queue;
}
return HSA_STATUS_SUCCESS;
}
#endif
hsa_status_t
destroy_queue(hsa_queue_t* hsa_queue)
{
auto* registration = get_queue_registration();
if(registration)
{
auto node = queue_collection_t::node_type{};
auto snapshot = std::vector<queue_cb_entry_t>{};
{
std::lock_guard lg(registration->mutex);
node = registration->queues.extract(hsa_queue);
if(!node.empty())
{
snapshot = std::vector<queue_cb_entry_t>{registration->cb_list};
}
else
{
ROCP_WARNING << "Destroy queue was called for a handle that was not in queues: "
<< hsa_queue;
}
}
if(!node.empty())
{
for(auto& cb_entry : snapshot)
{
if(cb_entry.cb)
{
cb_entry.cb(hsa_queue,
node.mapped().agent,
ROCPROFILER_ATTACH_QUEUE_DESTROYED,
cb_entry.data);
}
}
}
// node goes out of scope here: queue_entry_t is destroyed after callbacks (and
// any sync() called within them) have completed
}
return HSA_STATUS_SUCCESS;
}
int
iterate_all_queues(rocprof_attach_queue_iterator_t func, void* user_data)
{
auto* registration = CHECK_NOTNULL(get_queue_registration());
std::lock_guard lg(registration->mutex);
for(const auto& qr_pair : registration->queues)
{
func(qr_pair.first, qr_pair.second.agent, user_data);
}
return ROCPROFILER_STATUS_SUCCESS;
}
int
set_write_interceptor(hsa_queue_t* queue, write_interceptor_t func, void* data)
{
auto* registration = CHECK_NOTNULL(get_queue_registration());
auto lg = std::lock_guard{registration->mutex};
auto qr_pair = registration->queues.find(queue);
if(qr_pair == registration->queues.end())
{
ROCP_ERROR << "couldn't find registration to set write interceptor for queue " << queue;
return ROCPROFILER_STATUS_ERROR_INVALID_ARGUMENT;
}
qr_pair->second.user_write_interceptor_func = func;
qr_pair->second.user_write_interceptor_data = data;
return 0;
}
int
add_queue_cb(rocprofiler_attach_queue_cb_t cb, void* data)
{
if(!cb)
{
return ROCPROFILER_STATUS_ERROR_INVALID_ARGUMENT;
}
auto* registration = CHECK_NOTNULL(get_queue_registration());
auto snapshot = std::vector<std::pair<hsa_queue_t*, hsa_agent_t>>{};
{
auto lg = std::lock_guard{registration->mutex};
registration->cb_list.push_back({cb, data});
for(const auto& qr_pair : registration->queues)
{
snapshot.emplace_back(qr_pair.first, qr_pair.second.agent);
}
}
for(const auto& [queue, agent] : snapshot)
{
cb(queue, agent, ROCPROFILER_ATTACH_QUEUE_CREATED, data);
}
return ROCPROFILER_STATUS_SUCCESS;
}
int
remove_queue_cb(rocprofiler_attach_queue_cb_t cb)
{
if(!cb)
{
return ROCPROFILER_STATUS_ERROR_INVALID_ARGUMENT;
}
auto* registration = CHECK_NOTNULL(get_queue_registration());
auto lg = std::lock_guard{registration->mutex};
auto pred = [cb](const queue_cb_entry_t& e) { return e.cb == cb; };
registration->cb_list.erase(
std::remove_if(registration->cb_list.begin(), registration->cb_list.end(), pred),
registration->cb_list.end());
// Returns SUCCESS whether or not cb was found, to simplify cleanup paths.
return ROCPROFILER_STATUS_SUCCESS;
}
} // namespace
namespace rocprofiler
{
namespace attach
{
void
queue_registration_init(HsaApiTable* table)
{
ROCP_TRACE << "Initializing Queue Registration";
auto* registration = CHECK_NOTNULL(get_queue_registration());
CoreApiTable& core_table = *table->core_;
core_table.hsa_queue_create_fn = create_queue;
core_table.hsa_queue_destroy_fn = destroy_queue;
#if defined(HSA_AMD_EXT_API_TABLE_STEP_VERSION) && HSA_AMD_EXT_API_TABLE_STEP_VERSION >= 0x10
// Save the real hsa_amd_queue_create before overwriting it, so non-compute queue requests can
// be forwarded, then intercept descriptor-based queue creation.
registration->hsa_amd_queue_create_fn = *table->amd_ext_->hsa_amd_queue_create_fn;
table->amd_ext_->hsa_amd_queue_create_fn = create_amd_queue;
#endif
registration->hsa_amd_queue_intercept_create_fn =
*table->amd_ext_->hsa_amd_queue_intercept_create_fn;
registration->hsa_amd_profiling_set_profiler_enabled_fn =
*table->amd_ext_->hsa_amd_profiling_set_profiler_enabled_fn;
registration->hsa_amd_queue_intercept_register_fn =
*table->amd_ext_->hsa_amd_queue_intercept_register_fn;
registration->hsa_status_string_fn = *table->core_->hsa_status_string_fn;
}
} // namespace attach
} // namespace rocprofiler
ROCPROFILER_EXTERN_C_INIT
int
rocprofiler_attach_iterate_all_queues(rocprof_attach_queue_iterator_t func, void* data)
{
return iterate_all_queues(func, data);
}
int
rocprofiler_attach_set_write_interceptor(hsa_queue_t* queue, write_interceptor_t func, void* data)
{
return set_write_interceptor(queue, func, data);
}
int
rocprofiler_attach_add_queue_cb(rocprofiler_attach_queue_cb_t cb, void* data)
{
return add_queue_cb(cb, data);
}
int
rocprofiler_attach_remove_queue_cb(rocprofiler_attach_queue_cb_t cb)
{
return remove_queue_cb(cb);
}
ROCPROFILER_EXTERN_C_FINI