Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions projects/rocprofiler-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Full documentation for ROCprofiler-SDK is available at [rocm.docs.amd.com/projec

### Changed

- Kernel dispatch tracing with inline queue interposition places the completion signals it adds in GPU memory, except for the last dispatch of each submitted batch, where `hsa_amd_signal_batch_create` supports it (large-BAR gfx94x, gfx95x and gfx125x). This lowers the per-kernel overhead of tracing back-to-back kernels, such as HIP graph replay. `ROCPROFILER_DEVICE_SIGNALS=false` restores host signals.
- `rocprofiler_flush_buffer` on a buffer receiving kernel dispatch records waits, bounded, for the records of batches already submitted when device signals are in use.

### Resolved issues

### Known issues
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ automatically based on the services your tool enables.
If you set this to ``true`` while a context that requires the legacy
path (counter collection, ATT, or PC sampling) is registered, the SDK
logs a warning and falls back to the legacy path anyway.
* - ``ROCPROFILER_DEVICE_SIGNALS``
- ``true``
- Boolean (``true``/``false``). With inline queue interposition, gives the
dispatches the SDK adds a completion signal to a signal in the GPU's own
memory, except the last dispatch of each submitted batch, so the command
processor's completion write does not cross PCIe. Applies only where
``hsa_amd_signal_batch_create`` reports a signal-capable pool of GPU
memory (large-BAR gfx94x, gfx95x and gfx125x); elsewhere host signals
are used as before. Read once, when the SDK initializes HSA.

Kernel dispatch timestamp source
--------------------------------
Expand Down
17 changes: 17 additions & 0 deletions projects/rocprofiler-sdk/source/lib/rocprofiler-sdk/buffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "lib/rocprofiler-sdk/context/context.hpp"
#include "lib/rocprofiler-sdk/context/domain.hpp"
#include "lib/rocprofiler-sdk/hsa/hsa.hpp"
#include "lib/rocprofiler-sdk/hsa/queue_interposition.hpp"
#include "lib/rocprofiler-sdk/internal_threading.hpp"
#include "lib/rocprofiler-sdk/pc_sampling/service.hpp"
#include "lib/rocprofiler-sdk/registration.hpp"
Expand Down Expand Up @@ -66,6 +67,20 @@ get_buffer_offset()
}();
return _v;
}

// Whether a context delivers kernel dispatch records into @p buffer_id.
bool
receives_kernel_dispatch_records(rocprofiler_buffer_id_t buffer_id)
{
for(const auto* ctx : context::get_registered_contexts())
{
if(ctx->buffered_tracer &&
ctx->buffered_tracer->buffer_data.at(ROCPROFILER_BUFFER_TRACING_KERNEL_DISPATCH)
.handle == buffer_id.handle)
return true;
}
return false;
}
} // namespace

bool
Expand Down Expand Up @@ -330,6 +345,8 @@ rocprofiler_flush_buffer(rocprofiler_buffer_id_t buffer_id)
if(status != ROCPROFILER_STATUS_SUCCESS) return status;
#endif

if(rocprofiler::buffer::receives_kernel_dispatch_records(buffer_id))
rocprofiler::hsa::queue_interposition::flush_sync();
return rocprofiler::buffer::flush(buffer_id, true);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ set(ROCPROFILER_LIB_HSA_HEADERS
hsa_barrier.hpp
hsa.hpp
pc_sampling.cpp
pending_batches.hpp
profile_serializer.hpp
queue_controller.hpp
queue_interposition.hpp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,56 @@ init_gpu_pool(const AmdExtTable& api, rocprofiler::hsa::AgentCache& agent)
<< "HSA GPU Pool is not initialized";
}

// Finds the pool of the GPU's own memory that hsa_amd_signal_batch_create can place signals in.
// ROCr reports one only where the host can map it (large BAR): the extended-scope fine-grained pool
// on gfx94x/gfx95x, the coarse-grained pool on gfx125x. Completion signals there are written by
// the CP without crossing PCIe.
void
init_signal_pool(const AmdExtTable& api, rocprofiler::hsa::AgentCache& agent)
{
#if HSA_AMD_EXT_API_TABLE_STEP_VERSION >= 0x17
// read once, for the first agent: all agents of a process behave the same
static bool enabled = rocprofiler::common::get_env("ROCPROFILER_DEVICE_SIGNALS", true);
if(!enabled || api.version.step_id < 0x17 || !api.hsa_amd_signal_batch_create_fn ||
!api.hsa_amd_memory_pool_get_info_fn ||
!api.hsa_amd_profiling_convert_tick_to_system_domain_fn)
return;

auto find = [](hsa_amd_memory_pool_t pool, void* data) {
auto [api_ptr, pool_ptr] =
*static_cast<std::pair<const AmdExtTable*, hsa_amd_memory_pool_t*>*>(data);
bool capable = false;
auto location = HSA_AMD_MEMORY_POOL_LOCATION_CPU;
if(api_ptr->hsa_amd_memory_pool_get_info_fn(
pool, HSA_AMD_MEMORY_POOL_INFO_SIGNAL_CAPABLE, &capable) != HSA_STATUS_SUCCESS ||
!capable ||
api_ptr->hsa_amd_memory_pool_get_info_fn(
pool, HSA_AMD_MEMORY_POOL_INFO_LOCATION, &location) != HSA_STATUS_SUCCESS ||
location != HSA_AMD_MEMORY_POOL_LOCATION_GPU)
return HSA_STATUS_SUCCESS;
*pool_ptr = pool;
return HSA_STATUS_INFO_BREAK;
};

auto params = std::make_pair(&api, &agent.signal_pool());
api.hsa_amd_agent_iterate_memory_pools_fn(agent.get_hsa_agent(), find, &params);

if(agent.signal_pool().handle != 0)
{
auto flags = uint32_t{0};
api.hsa_amd_memory_pool_get_info_fn(
agent.signal_pool(), HSA_AMD_MEMORY_POOL_INFO_GLOBAL_FLAGS, &flags);
ROCP_INFO << fmt::format("agent {}: kernel dispatch completion signals in device memory "
"pool {:#x} (global flags {:#x})",
agent.name(),
agent.signal_pool().handle,
flags);
}
#else
common::consume_args(api, agent);
#endif
}

} // namespace

namespace rocprofiler
Expand Down Expand Up @@ -180,6 +230,7 @@ AgentCache::AgentCache(const rocprofiler_agent_t* rocp_agent,
{
init_cpu_pool(ext_table, *this);
init_gpu_pool(ext_table, *this);
init_signal_pool(ext_table, *this);
// When on-demand queue mode is enabled, defer queue creation to start_context
if(!use_ondemand_queue()) init_device_counting_service_queue(api, ext_table);
} catch(std::runtime_error& e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class AgentCache
CONST_NONCONST_ACCESSOR(hsa_amd_memory_pool_t, cpu_pool, m_cpu_pool);
CONST_NONCONST_ACCESSOR(hsa_amd_memory_pool_t, kernarg_pool, m_kernarg_pool);
CONST_NONCONST_ACCESSOR(hsa_amd_memory_pool_t, gpu_pool, m_gpu_pool);
CONST_NONCONST_ACCESSOR(hsa_amd_memory_pool_t, signal_pool, m_signal_pool);
CONST_NONCONST_ACCESSOR(hsa_agent_t, get_hsa_agent, m_hsa_agent);
CONST_NONCONST_ACCESSOR(hsa_agent_t, near_cpu, m_nearest_cpu);

Expand All @@ -91,6 +92,8 @@ class AgentCache
hsa_amd_memory_pool_t m_cpu_pool{.handle = 0};
hsa_amd_memory_pool_t m_kernarg_pool{.handle = 0};
hsa_amd_memory_pool_t m_gpu_pool{.handle = 0};
// device pool hsa_amd_signal_batch_create accepts, zero if none
hsa_amd_memory_pool_t m_signal_pool{.handle = 0};

std::string_view m_name = {};
mutable hsa_queue_t* m_profile_queue = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// MIT License
//
// Copyright (c) 2026 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.

#pragma once

#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <map>
#include <mutex>

namespace rocprofiler
{
namespace hsa
{
/**
* @brief Batches whose dispatch records are not out yet, by registration order.
*
* Batches complete in any order (different queues, short after long), so a count of finished
* batches cannot tell whether a given one is done. This keeps the identity of each, so a buffer
* flush can wait for exactly the batches added before it.
*/
class pending_batches
{
public:
using clock_type = std::chrono::steady_clock;
using duration_type = clock_type::duration;

// A batch still running on the device.
uint64_t add()
{
auto lk = std::lock_guard<std::mutex>{m_mtx};
auto id = m_next_id++;
m_pending.emplace(id, false);
return id;
}

// The device finished the batch and its records are being emitted. Id 0 is never tracked.
void set_emitting(uint64_t id)
{
if(id == 0) return;
{
auto lk = std::lock_guard<std::mutex>{m_mtx};
auto it = m_pending.find(id);
if(it == m_pending.end() || it->second) return;
it->second = true;
++m_transitions;
}
m_cv.notify_all();
}

// The batch's records are out.
void remove(uint64_t id)
{
if(id == 0) return;
{
auto lk = std::lock_guard<std::mutex>{m_mtx};
if(m_pending.erase(id) == 0) return;
++m_transitions;
}
m_cv.notify_all();
}

/**
* @brief Wait until every batch added before this call is removed.
*
* Gives up once nothing has progressed for @p device_timeout while all of those batches are
* still on the device (a dispatch that is still running), or for @p emit_timeout while some
* are being emitted (a tool callback that does not return).
*
* @return whether every such batch was removed
*/
bool wait_for_added(duration_type device_timeout, duration_type emit_timeout)
{
auto lk = std::unique_lock<std::mutex>{m_mtx};
const auto target = m_next_id;
auto seen = m_transitions;
auto last_progress = clock_type::now();

while(true)
{
const auto end = m_pending.lower_bound(target);
if(m_pending.begin() == end) return true;

const bool emitting =
std::any_of(m_pending.begin(), end, [](const auto& itr) { return itr.second; });
const auto deadline = last_progress + (emitting ? emit_timeout : device_timeout);

m_cv.wait_until(lk, deadline, [&]() { return m_transitions != seen; });
if(m_transitions != seen)
{
seen = m_transitions;
last_progress = clock_type::now();
}
else if(clock_type::now() >= deadline)
{
return false;
}
}
}

bool empty() const
{
auto lk = std::lock_guard<std::mutex>{m_mtx};
return m_pending.empty();
}

private:
mutable std::mutex m_mtx = {};
std::condition_variable m_cv = {};
std::map<uint64_t, bool> m_pending = {};
uint64_t m_next_id = 1;
uint64_t m_transitions = 0;
};
} // namespace hsa
} // namespace rocprofiler
Loading
Loading