Files
rocm-systems/projects/aqlprofile/src/core/spm_v2.cpp
T
Bing Ma 171a5f5bda [aqlprofile] Enable SPM support for MI200/MI300 (#1768)
* [SPM] Enable legacy SPM aqlprofile API

* [SPM] Enable SPM aqlprofile_v2 API

* [NPI][SPM] Fix crash from ctrl test

* Adding decode v1 (#189)

Co-authored-by: Giovanni baraldi <gbaraldi@amd.com>

* Fix various issues on MI200
1. RLC_SPM_PERFMON_SEGMENT_SIZE_CORE1 support
2. ActiveCU patch for SPM delay table

* [SPM] Fix wrong SPM counter values on MI3xx

* Add mode and query blocks (#196)

Co-authored-by: Giovanni baraldi <gbaraldi@amd.com>

* [aqlprofile][spm] Use existing SpmBlockId enum info for delay table size

* [aqlprofile][spm] Remove obsolete logic

* Update projects/aqlprofile/src/core/include/aqlprofile-sdk/aql_profile_v2.h

---------

Co-authored-by: Baraldi, Giovanni <Giovanni.Baraldi@amd.com>
Co-authored-by: Giovanni baraldi <gbaraldi@amd.com>
2025-11-19 11:17:01 -08:00

522 wiersze
19 KiB
C++

#include "hsa/hsa_ext_amd.h"
#include "include/aqlprofile-sdk/aql_profile_v2.h"
#include "include/spm_common.hpp"
#include "memorymanager.hpp"
#include "core/commandbuffermgr.hpp"
#include <thread>
#include <condition_variable>
#include "core/logger.h"
#include "core/pm4_factory.h"
#include <map>
#include <array>
#include <shared_mutex>
#define PUBLIC_API __attribute__((visibility("default")))
static void producer(std::shared_ptr<class spm_state_t> s);
static void consumer(std::shared_ptr<class spm_state_t> s, aqlprofile_spm_data_callback_t callback, void* userdata);
#define CHECKHSA(x, action) { \
auto _status = (x); \
if (_status != HSA_STATUS_SUCCESS) { \
std::cerr << __FILE__ << ':' << __LINE__ << " error:" << _status << std::endl; \
action; \
} \
}
struct spm_set_dest_buffer_args {
hsa_agent_t hsa_agent{0};
size_t buf_size{0};
uint32_t timeout{0};
uint32_t size_copied{0};
void* dest_buf{nullptr};
bool is_data_loss{false};
};
struct spm_state_t : public spm_set_dest_buffer_args {
aqlprofile_agent_handle_t aql_agent{};
std::thread* manager_thread{nullptr};
std::mutex work_mutex{};
std::condition_variable work_cond{};
std::atomic<bool> data_ready{};
std::atomic<int> signal_data_loss{};
std::atomic<bool> stop_prod_thread{};
std::atomic<bool> stop_cons_thread{};
std::atomic<void*> prod_buf{nullptr};
std::atomic<void*> cons_buf{nullptr};
uint32_t num_xcc{0};
size_t buf_size_xcc{0};
void* output_buffer_ptr{nullptr};
size_t output_buffer_size{0};
std::unique_ptr<SPMMemoryManager> memory{nullptr};
std::array<size_t, AQLPROFILE_SPM_PARAMETER_TYPE_LAST> parameters;
};
inline static hsa_status_t HsaSpmSetDestBuffer(spm_set_dest_buffer_args& args) {
if (args.hsa_agent.handle == 0) throw std::runtime_error("Invalid hsa agent");
return hsa_amd_spm_set_dest_buffer(args.hsa_agent, args.buf_size, &args.timeout, &args.size_copied,
args.dest_buf, &args.is_data_loss);
}
class ManagerThread
{
public:
ManagerThread(std::shared_ptr<spm_state_t> _s, aqlprofile_spm_data_callback_t cb, void* userdata)
: s(_s), agent(_s->hsa_agent)
{
if (agent.handle == 0) throw std::runtime_error("Invalid hsa agent");
s->stop_cons_thread = false;
s->stop_prod_thread = false;
status = hsa_amd_spm_acquire(s->hsa_agent);
CHECKHSA(status, return);
// This non-blocking (timeout = 0) HsaSpmSetDestBuffer() call will clear up all the
// residual counters from previous SPM runs. Most of the time, nothing will be copied.
// This call will also trigger KFD to call spm_start() function. We must make sure
// spm_start() is finished before we give back the control to caller of
// start_spm_threads().
spm_set_dest_buffer_args args = *s;
args.size_copied = 0;
args.timeout = 0;
if (HsaSpmSetDestBuffer(args) != HSA_STATUS_SUCCESS)
throw std::runtime_error("hsa_amd_spm_set_dest_buffer() init error");
producer_thread = std::thread(producer, s);
consumer_thread = std::thread(consumer, s, cb, userdata);
}
~ManagerThread()
{
s->stop_prod_thread.store(true);
if (producer_thread.joinable()) producer_thread.join();
if (consumer_thread.joinable()) consumer_thread.join();
hsa_amd_spm_release(this->agent);
}
hsa_status_t status = HSA_STATUS_ERROR;
private:
std::thread producer_thread{};
std::thread consumer_thread{};
std::shared_ptr<spm_state_t> s{nullptr};
hsa_agent_t agent;
};
namespace aqlprofile
{
namespace spm
{
std::vector<aqlprofile_spm_parameter_t> default_spm_params = {
{AQLPROFILE_SPM_PARAMETER_TYPE_BUFFER_SIZE, 1<<26}, // 64MB
{AQLPROFILE_SPM_PARAMETER_TYPE_SAMPLE_INTERVAL, 1<<13}, // 4us
{AQLPROFILE_SPM_PARAMETER_TYPE_TIMEOUT, 100}, // 100ms
{AQLPROFILE_SPM_PARAMETER_TYPE_SAMPLE_MODE, AQLPROFILE_SPM_PARAMETER_SAMPLE_MODE_SCLK}
};
static_assert(AQLPROFILE_SPM_PARAMETER_TYPE_LAST == 4 && "Dont forget to add default param!");
counter_des_t GetCounter(
aql_profile::Pm4Factory* pm4_factory,
const aqlprofile_pmc_event_t& event,
std::map<block_des_t, uint32_t, lt_block_des>& index_map
) {
const GpuBlockInfo* block_info = pm4_factory->GetBlockInfo(event.block_name);
const block_des_t block_des = {block_info->id, event.block_index};
const auto ret = index_map.insert({block_des, 0});
auto reg_index = ret.first->second;
if (reg_index >= block_info->counter_count)
throw std::runtime_error("Event is out of block counter registers number limit");
ret.first->second++;
return {event.event_id, reg_index, block_des, block_info};
}
pm4_builder::counters_vector CountersVec(
const aqlprofile_pmc_event_t* events,
size_t num_events,
aql_profile::Pm4Factory* pm4_factory
) {
pm4_builder::counters_vector vec;
std::map<block_des_t, uint32_t, lt_block_des> index_map;
for (size_t i=0; i<num_events; i++)
vec.push_back(GetCounter(pm4_factory, events[i], index_map));
return vec;
}
class SpmStateMap
{
public:
std::shared_ptr<spm_state_t> query(aqlprofile_handle_t handle)
{
auto lock = std::shared_lock{mut};
auto it = map.find(handle);
if (it != map.end()) return it->second;
return nullptr;
}
void insert(aqlprofile_handle_t handle, std::shared_ptr<spm_state_t> state)
{
auto lock = std::unique_lock{mut};
map.emplace(handle, std::move(state));
}
void remove(aqlprofile_handle_t handle)
{
auto lock = std::unique_lock{mut};
try
{
map.at(handle)->manager_thread = nullptr;
map.at(handle)->memory = nullptr;
map.erase(handle);
}
catch(...) {}
}
bool setthread(aqlprofile_handle_t handle, std::unique_ptr<ManagerThread>&& thread)
{
auto lock = std::unique_lock{mut};
bool bret = threads.find(handle) != threads.end();
threads[handle] = std::move(thread);
return bret;
}
private:
std::shared_mutex mut;
std::map<aqlprofile_handle_t, std::shared_ptr<spm_state_t>> map{};
std::map<aqlprofile_handle_t, std::unique_ptr<ManagerThread>> threads{};
};
auto* spm_state_map = new SpmStateMap{};
hsa_status_t _internal_aqlprofile_spm_create_packets(
aqlprofile_handle_t* handle,
aqlprofile_spm_buffer_desc_t* out_desc,
aqlprofile_spm_aql_packets_t* packets,
aqlprofile_spm_profile_t profile,
size_t flags
) {
auto s = std::make_shared<spm_state_t>();
s->aql_agent = profile.aql_agent;
s->hsa_agent = profile.hsa_agent;
auto& params = s->parameters;
for (auto& p : default_spm_params) params.at(p.type) = p.value; // Set default params
try
{
for (size_t i=0; i<profile.parameter_count; i++)
params.at(profile.parameters[i].type) = profile.parameters[i].value;
}
catch(...) { return HSA_STATUS_ERROR_INVALID_ARGUMENT; }
s->memory = std::make_unique<SPMMemoryManager>(profile.aql_agent, profile.hsa_agent, profile.alloc_cb, profile.dealloc_cb, profile.userdata);
auto& memory = s->memory;
try
{
memory->CreateOutputBuf(params.at(AQLPROFILE_SPM_PARAMETER_TYPE_BUFFER_SIZE)+SPM_DESC_SIZE);
}
catch(...) { return HSA_STATUS_ERROR_OUT_OF_RESOURCES; }
// Populate user output
handle->handle = memory->GetHandler();
out_desc->data = memory->GetOutputBuf();
out_desc->size = SPM_DESC_SIZE;
spm_state_map->insert(*handle, s);
{
aql_profile::Pm4Factory* pm4_factory = nullptr;
try
{
pm4_factory = aql_profile::Pm4Factory::Create(profile.aql_agent);
if (!pm4_factory) throw std::exception();
}
catch(...) { return HSA_STATUS_ERROR_INVALID_AGENT; }
const pm4_builder::counters_vector countersVec = CountersVec(profile.events, profile.event_count, pm4_factory);
pm4_builder::TraceConfig& trace_config = memory->config;
trace_config.spm_sq_32bit_mode = true;
trace_config.spm_has_core1 = (pm4_factory->GetGpuId() == aql_profile::MI100_GPU_ID) ||
(pm4_factory->GetGpuId() == aql_profile::MI200_GPU_ID);
trace_config.spm_sample_delay_max = pm4_factory->GetSpmSampleDelayMax();
trace_config.sampleRate = (s->parameters.at(AQLPROFILE_SPM_PARAMETER_TYPE_SAMPLE_INTERVAL) + 16) & ~31ul;
if (trace_config.sampleRate == 0) return HSA_STATUS_ERROR_INVALID_ARGUMENT;
if (s->parameters.at(AQLPROFILE_SPM_PARAMETER_TYPE_SAMPLE_MODE) != AQLPROFILE_SPM_PARAMETER_SAMPLE_MODE_SCLK)
return HSA_STATUS_ERROR_INVALID_ARGUMENT;
trace_config.xcc_number = pm4_factory->GetXccNumber();
trace_config.se_number = pm4_factory->GetShaderEnginesNumber() / trace_config.xcc_number;
trace_config.sa_number = pm4_factory->GetGpuId() >= aql_profile::GFX10_GPU_ID ? 2 : 0;
trace_config.data_buffer_ptr = memory->GetOutputBuf();
trace_config.data_buffer_size = memory->GetOutputBufSize();
pm4_builder::CmdBuffer start_cmd;
pm4_builder::CmdBuffer stop_cmd;
pm4_builder::SpmBuilder* spm_builder = pm4_factory->GetSpmBuilder();
// Generate commands
spm_builder->Begin(&start_cmd, &trace_config, countersVec);
spm_builder->End(&stop_cmd, &trace_config);
// Copy generated commands
size_t start_size = aql_profile::CommandBufferMgr::Align(start_cmd.Size());
size_t stop_size = aql_profile::CommandBufferMgr::Align(stop_cmd.Size());
try
{
memory->CreateCmdBuf(start_size+stop_size);
}
catch(...) { return HSA_STATUS_ERROR_OUT_OF_RESOURCES; }
pm4_builder::CmdBuilder* cmd_writer = pm4_factory->GetCmdBuilder();
uint8_t* cmdbuf = reinterpret_cast<uint8_t*>(memory->GetCmdBuf());
profile.memcpy_cb(cmdbuf, start_cmd.Data(), start_cmd.Size(), profile.userdata);
aql_profile::PopulateAql(cmdbuf, start_cmd.Size(), cmd_writer, &packets->start_packet);
cmdbuf += start_size;
profile.memcpy_cb(cmdbuf, stop_cmd.Data(), stop_cmd.Size(), profile.userdata);
aql_profile::PopulateAql(cmdbuf, stop_cmd.Size(), cmd_writer, &packets->stop_packet);
}
s->output_buffer_ptr = memory->GetOutputBuf();
s->output_buffer_size = memory->GetOutputBufSize();
return HSA_STATUS_SUCCESS;
}
} // namespace spm
} // namespace aqlprofile
PUBLIC_API hsa_status_t aqlprofile_spm_create_packets(
aqlprofile_handle_t* handle,
aqlprofile_spm_buffer_desc_t* out_desc,
aqlprofile_spm_aql_packets_t* packets,
aqlprofile_spm_profile_t profile,
size_t flags
) {
try
{
return aqlprofile::spm::_internal_aqlprofile_spm_create_packets(handle, out_desc, packets, profile, flags);
}
catch(...) { return HSA_STATUS_ERROR; }
return HSA_STATUS_SUCCESS;
}
PUBLIC_API hsa_status_t aqlprofile_spm_start(
aqlprofile_handle_t handle,
aqlprofile_spm_data_callback_t data_cb,
void* userdata
) {
auto s = aqlprofile::spm::spm_state_map->query(handle);
if (!s) return HSA_STATUS_ERROR_NOT_INITIALIZED;
// The first page of output_buffer is reserved for SpmBufferDesc
char* buf_ptr = (char*)(s->output_buffer_ptr) + SPM_DESC_SIZE;
size_t buf_size = (s->output_buffer_size - SPM_DESC_SIZE) / 3;
SpmBufferDesc* desc = (SpmBufferDesc*)s->output_buffer_ptr;
size_t seg_size = (desc->global_num_line + desc->se_num_line * desc->num_se) * 32;
// Align buf_size to the exact multiples of segments, so that every HsaSpmSetDestBuffer
// will always return complete segments
if (!desc->num_xcc) desc->num_xcc = 1;
buf_size /= desc->num_xcc;
if (seg_size) {
buf_size = (buf_size - sizeof(kfd_ioctl_spm_buffer_header)) / seg_size * seg_size +
sizeof(kfd_ioctl_spm_buffer_header);
}
buf_size *= desc->num_xcc;
// Args for hsa_amd_spm_set_dest_buffer
s->buf_size = buf_size;
s->timeout = s->parameters.at(AQLPROFILE_SPM_PARAMETER_TYPE_TIMEOUT);
s->dest_buf = buf_ptr;
s->prod_buf = buf_ptr + buf_size;
s->cons_buf = buf_ptr + buf_size * 2;
s->num_xcc = desc->num_xcc;
s->buf_size_xcc = s->buf_size / desc->num_xcc;
try
{
auto manager = std::make_unique<ManagerThread>(s, data_cb, userdata);
CHECKHSA(manager->status, return manager->status);
aqlprofile::spm::spm_state_map->setthread(handle, std::move(manager));
}
catch(...) { return HSA_STATUS_ERROR; }
return HSA_STATUS_SUCCESS;
}
PUBLIC_API hsa_status_t aqlprofile_spm_stop(aqlprofile_handle_t handle)
{
bool b = aqlprofile::spm::spm_state_map->setthread(handle, nullptr);
return b ? HSA_STATUS_SUCCESS : HSA_STATUS_ERROR_NOT_INITIALIZED;
}
PUBLIC_API void aqlprofile_spm_delete_packets(aqlprofile_handle_t handle)
{
aqlprofile::spm::spm_state_map->remove(handle);
}
struct consumer_thread_handle_t
{
consumer_thread_handle_t(std::shared_ptr<spm_state_t> _s): s(std::move(_s)) {};
~consumer_thread_handle_t()
{
s->stop_cons_thread = true;
s->work_cond.notify_one();
}
void notify()
{
s->data_ready = true;
s->work_cond.notify_one();
}
std::shared_ptr<spm_state_t> s;
};
static void producer(std::shared_ptr<spm_state_t> s)
{
hsa_status_t status = HSA_STATUS_SUCCESS;
spm_set_dest_buffer_args args = *s;
bool exiting = false;
int count_down = 0;
consumer_thread_handle_t consumer_handle(s);
args.timeout = s->timeout;
while(true)
{
args.size_copied = 0;
args.dest_buf = s->prod_buf;
// s->stop_prod_thread should be set after SPM End() sequence is submitted, this is the
// handshake protocal between app/library and aqlprofile.
// If s->stop_prod_thread is set in current loop, producer thread will exit after all
// SPM counters are drained (args.size_copied == 0) which could be at least one
// HsaSpmSetDestBuffer() call or maybe more than one.
if (s->stop_prod_thread) exiting = true;
if (HsaSpmSetDestBuffer(args) != HSA_STATUS_SUCCESS)
{
std::unique_lock<std::mutex> lock(s->work_mutex);
std::cerr << "hsa_amd_spm_set_dest_buffer() error" << std::endl;
s->size_copied = 0;
consumer_handle.notify();
return;
}
{
std::unique_lock<std::mutex> lock(s->work_mutex);
s->dest_buf = s->prod_buf.exchange(s->cons_buf.exchange(s->dest_buf));
// In the initial XCC SPM design, 'size_copied' and 'is_data_loss' are stored in
// kfd_ioctl_spm_buffer_header. They are no longer stored in kfd_ioctl_spm_args.
// But we still need accumulated version for some quick checks and KFD will add
// them back to kfd_ioctl_spm_args.
// This is only a temporary patch as KFD will fix this in ROCm 6.5
char* base = (char*)s->cons_buf.load();
s->size_copied = 0;
s->is_data_loss = false;
for (int i = 0; i < s->num_xcc; i++) {
auto buf_info = (kfd_ioctl_spm_buffer_header*)base;
s->size_copied += buf_info->bytes_copied;
s->is_data_loss |= buf_info->has_data_loss;
base += s->buf_size_xcc;
}
s->signal_data_loss.fetch_or(s->is_data_loss);
consumer_handle.notify();
}
if (exiting)
{
// Forced exit: This happens when we want to stop SPM but not the app. This should be
// improved by getting the hint from caller instead of a hardcoded number. Will consider this
// in the new SPM api design
if (s->size_copied)
{
if (count_down++ < 5) continue;
printf("Forced exit after %d extra hsa_amd_spm_set_dest_buffer() calls\n", count_down);
}
// We cannot directly use s->stop_prod_thread here, otherwise we might miss the last
// HsaSpmSetDestBuffer() call if s->stop_prod_thread is set after the HsaSpmSetDestBuffer()
// call from this loop!
//
break;
}
if (s->stop_cons_thread) break;
}
}
static void consumer(std::shared_ptr<spm_state_t> s, aqlprofile_spm_data_callback_t callback, void* userdata)
{
while (true)
{
std::unique_lock<std::mutex> lock(s->work_mutex);
s->work_cond.wait(lock, [&s](){ return s->data_ready || s->stop_cons_thread; });
if (!s->data_ready) return;
s->data_ready = false;
char* base = (char*)s->cons_buf.load();
int flags = s->signal_data_loss.exchange(0)<<AQLPROFILE_SPM_DATA_FLAGS_DATA_LOSS;
for (int i = 0; i < s->num_xcc; i++)
{
auto buf_info = (kfd_ioctl_spm_buffer_header*)base;
if (buf_info->bytes_copied)
callback(i, (void*)(buf_info + 1), buf_info->bytes_copied, flags, userdata);
base += s->buf_size_xcc;
}
}
}
PUBLIC_API bool
aqlprofile_spm_is_event_supported(aqlprofile_agent_handle_t agent, aqlprofile_pmc_event_t event)
{
aql_profile::Pm4Factory* pm4_factory = nullptr;
try
{
pm4_factory = aql_profile::Pm4Factory::Create(agent);
if (!pm4_factory) return false;
}
catch(...) { return false; }
if (pm4_factory->GetGpuId() < aql_profile::MI200_GPU_ID || pm4_factory->GetGpuId() > aql_profile::MI350_GPU_ID)
return false;
static auto blocks = []()
{
std::array<bool, AQLPROFILE_BLOCKS_NUMBER> valid_blocks{};
valid_blocks[HSA_VEN_AMD_AQLPROFILE_BLOCK_NAME_CPC] = true;
valid_blocks[HSA_VEN_AMD_AQLPROFILE_BLOCK_NAME_CPF] = true;
valid_blocks[HSA_VEN_AMD_AQLPROFILE_BLOCK_NAME_SQ] = true;
valid_blocks[HSA_VEN_AMD_AQLPROFILE_BLOCK_NAME_SPI] = true;
valid_blocks[HSA_VEN_AMD_AQLPROFILE_BLOCK_NAME_TCC] = true;
valid_blocks[HSA_VEN_AMD_AQLPROFILE_BLOCK_NAME_TCA] = true;
valid_blocks[HSA_VEN_AMD_AQLPROFILE_BLOCK_NAME_TCP] = true;
valid_blocks[HSA_VEN_AMD_AQLPROFILE_BLOCK_NAME_TA] = true;
valid_blocks[HSA_VEN_AMD_AQLPROFILE_BLOCK_NAME_TD] = true;
valid_blocks[HSA_VEN_AMD_AQLPROFILE_BLOCK_NAME_SPI] = true;
return valid_blocks;
}();
if (event.flags.spm_flags.depth != AQLPROFILE_SPM_DEPTH_NONE) return false;
if (event.block_name >= blocks.size()) return false;
return blocks.at(event.block_name);
}