Files
rocm-systems/runtime/hsa-runtime/core/runtime/intercept_queue.cpp
T
Saleel Kudchadker 3baaa6e9c0 rocr: Allocate AQL queue on device memory
- Use HSA_ALLOCATE_QUEUE_DEV_MEM=1 to create AQL queue in device
memory.
- Before writing AQL packet header to the queue use an SFENCE to ensure
that there is no reodering of the writes over PCIE

Change-Id: I5eacdc35108c4a1e245c75ae349b7495451aa60d
2024-09-05 17:48:02 -04:00

416 lines
18 KiB
C++

////////////////////////////////////////////////////////////////////////////////
//
// The University of Illinois/NCSA
// Open Source License (NCSA)
//
// Copyright (c) 2014-2020, Advanced Micro Devices, Inc. All rights reserved.
//
// Developed by:
//
// AMD Research and AMD HSA Software Development
//
// Advanced Micro Devices, Inc.
//
// www.amd.com
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal with 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:
//
// - Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimers.
// - Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimers in
// the documentation and/or other materials provided with the distribution.
// - Neither the names of Advanced Micro Devices, Inc,
// nor the names of its contributors may be used to endorse or promote
// products derived from this Software without specific prior written
// permission.
//
// 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 CONTRIBUTORS 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 WITH THE SOFTWARE.
//
////////////////////////////////////////////////////////////////////////////////
#include "core/inc/intercept_queue.h"
#include "core/inc/amd_aql_queue.h"
#include "core/util/utils.h"
#include "inc/hsa_api_trace.h"
namespace rocr {
namespace core {
namespace {
// Determine if a packet is the AMD_AQL_FORMAT_INTERCEPT_MARKER packet. Loads
// the packet header non-atomically. That is permissable if the calling thread
// has previously loaded the header atomically to determine if it is not an
// INVALID packet. Once a packet is no longer INVALID its ownership belongs to
// the packer processor.
bool inline IsInterceptMarkerPacket(const AqlPacket* packet) {
return (AqlPacket::type(packet->packet.header) == HSA_PACKET_TYPE_VENDOR_SPECIFIC) &&
(packet->amd_vendor.format == AMD_AQL_FORMAT_INTERCEPT_MARKER);
}
} // namespace
struct InterceptFrame {
InterceptQueue* queue;
uint64_t pkt_index;
size_t interceptor_index;
};
static thread_local InterceptFrame Cursor = {nullptr, 0, 0};
static const uint16_t kInvalidHeader = (HSA_PACKET_TYPE_INVALID << HSA_PACKET_HEADER_TYPE) |
(1 << HSA_PACKET_HEADER_BARRIER) |
(HSA_FENCE_SCOPE_NONE << HSA_PACKET_HEADER_ACQUIRE_FENCE_SCOPE) |
(HSA_FENCE_SCOPE_NONE << HSA_PACKET_HEADER_RELEASE_FENCE_SCOPE);
static const uint16_t kBarrierHeader = (HSA_PACKET_TYPE_BARRIER_AND << HSA_PACKET_HEADER_TYPE) |
(1 << HSA_PACKET_HEADER_BARRIER) |
(HSA_FENCE_SCOPE_NONE << HSA_PACKET_HEADER_ACQUIRE_FENCE_SCOPE) |
(HSA_FENCE_SCOPE_NONE << HSA_PACKET_HEADER_RELEASE_FENCE_SCOPE);
int InterceptQueue::rtti_id_ = 0;
bool InterceptQueue::IsPendingRetryPoint(uint64_t wrapped_current_read_index) const {
// This function is intended to determine if the last retry barrier packet
// has definitely not been processed in order to avoid putting multiple retry
// packets on the wrapped queue.
//
// The AQL protocol allows the packet processor to advance the read index any
// time after the producer advances the write index. It does not specify the
// latest that the read index must be advanced. This makes it impossible to
// use the read index to determine if a packet has definitely not been
// processed.
//
// This code assumes that the read index will be advanced no later than the
// start of processing the next packet. So at worst, if the read index equals
// the retry index the packet may have already been processed, and its
// completion signal updated (perhaps that was the cause of entering
// InterceptQueue::StoreRelaxed that is now invoking this function). But if
// the read index is less than the retry index, then the packet has not yet
// been processed, This implies that the minimum queue size is 3 (enforced in
// hsa_amd_queue_intercept_create): a non-retry packet, a retry packet that
// is being processed, and space for a new retry packet.
//
// FIXME: The above assumption can be removed by using a distinct interrupt
// signal for the retry packet completion signal, and tracking when that
// signal is updated and invokes its async handler. Currently the wrapped
// queue doorbell signal is also being used as the retry completion signal.
// If that is done then the minimum queue size needs to be changed from 3 to
// 2 (enforced in hsa_amd_queue_intercept_create).
return retry_index_ > wrapped_current_read_index;
}
InterceptQueue::InterceptQueue(std::unique_ptr<Queue> queue)
: QueueProxy(std::move(queue)),
LocalSignal(0, false),
DoorbellSignal(signal()),
next_packet_(0),
retry_index_(0),
quit_(false),
active_(true) {
// Initial retry_index_ value must ensure that
// InterceptQueue::IsPendingRetryPoint will return false before the first
// retry barrier packet is inserted.
assert(!IsPendingRetryPoint(next_packet_) &&
"Packet intercept error: initial retry index is incompatible with IsPendingRetryPoint.\n");
buffer_ = SharedArray<AqlPacket, 4096>(wrapped->amd_queue_.hsa_queue.size);
amd_queue_.hsa_queue.base_address = reinterpret_cast<void*>(&buffer_[0]);
// Fill the ring buffer with invalid packet headers.
// Leave packet content uninitialized to help trigger application errors.
for (uint32_t pkt_id = 0; pkt_id < wrapped->amd_queue_.hsa_queue.size; ++pkt_id) {
buffer_[pkt_id].packet.header = HSA_PACKET_TYPE_INVALID;
}
// Match the queue's signal ABI block to async_doorbell_'s
// This allows us to use the queue's signal ABI block from devices to trigger async_doorbell while
// host side use jumps directly to the queue's signal implementation.
async_doorbell_ = new InterruptSignal(DOORBELL_MAX);
MAKE_NAMED_SCOPE_GUARD(sigGuard, [&]() { async_doorbell_->DestroySignal(); });
this->signal_ = async_doorbell_->signal_;
amd_queue_.hsa_queue.doorbell_signal = Signal::Convert(this);
// Install an async handler for device side dispatches.
auto err = Runtime::runtime_singleton_->SetAsyncSignalHandler(
core::Signal::Convert(async_doorbell_), HSA_SIGNAL_CONDITION_NE,
async_doorbell_->LoadRelaxed(), HandleAsyncDoorbell, this);
if (err != HSA_STATUS_SUCCESS)
throw AMD::hsa_exception(err, "Doorbell handler registration failed.\n");
// Install copy submission interceptor.
AddInterceptor(Submit, this);
sigGuard.Dismiss();
}
InterceptQueue::~InterceptQueue() {
active_ = false;
// Kill the async doorbell handler
// Doorbell may not be used during or after queue destroy, however an interrupt may be in flight.
// Ensure doorbell value is not 0, mark for exit, wake handler and wait for termination value.
async_doorbell_->StoreRelaxed(DOORBELL_MAX);
quit_ = true;
hsa_signal_value_t val = async_doorbell_->ExchRelaxed(1);
if (val != 0)
async_doorbell_->WaitRelaxed(HSA_SIGNAL_CONDITION_EQ, 0, -1, HSA_WAIT_STATE_BLOCKED);
async_doorbell_->DestroySignal();
}
bool InterceptQueue::HandleAsyncDoorbell(hsa_signal_value_t value, void* arg) {
InterceptQueue* queue = reinterpret_cast<InterceptQueue*>(arg);
if (queue->quit_) {
queue->async_doorbell_->StoreRelaxed(0);
return false;
}
queue->async_doorbell_->StoreRelaxed(DOORBELL_MAX);
queue->StoreRelease(value);
return true;
}
void InterceptQueue::PacketWriter(const void* pkts, uint64_t pkt_count) {
assert(Cursor.interceptor_index > 0 &&
"Packet intercept error: final submit handler must not call PacketWritter.\n");
--Cursor.interceptor_index;
auto& handler = Cursor.queue->interceptors[Cursor.interceptor_index];
handler.first(pkts, pkt_count, Cursor.pkt_index, handler.second, PacketWriter);
// Restore index as the same rewrite handler may call the PacketWriter more than once.
++Cursor.interceptor_index;
}
void InterceptQueue::Submit(const void* pkts, uint64_t pkt_count, uint64_t user_pkt_index,
void* data, hsa_amd_queue_intercept_packet_writer writer) {
InterceptQueue* queue = reinterpret_cast<InterceptQueue*>(data);
const AqlPacket* packets = (const AqlPacket*)pkts;
// Submit final packet transform to hardware.
uint64_t submitted_count = queue->Submit(packets, pkt_count);
if (submitted_count == pkt_count) return;
// Could not submit all the final packets, stash unsubmitted ones for later.
assert(queue->overflow_.empty() && "Packet intercept error: overflow buffer not empty.\n");
for (uint64_t i = submitted_count; i < pkt_count; i++)
queue->overflow_.push_back(packets[i]);
}
uint64_t InterceptQueue::Submit(const AqlPacket* packets, uint64_t count) {
if (count == 0) return 0;
uint64_t marker_count = 0;
for (uint64_t i = 0; i < count; i++) {
if (IsInterceptMarkerPacket(&packets[i])) ++marker_count;
}
AqlPacket* ring = reinterpret_cast<AqlPacket*>(wrapped->amd_queue_.hsa_queue.base_address);
uint64_t mask = wrapped->amd_queue_.hsa_queue.size - 1;
while (true) {
uint64_t write = wrapped->LoadWriteIndexRelaxed();
uint64_t read = wrapped->LoadReadIndexRelaxed();
uint64_t free_slots = wrapped->amd_queue_.hsa_queue.size - (write - read);
bool pending_retry_point = IsPendingRetryPoint(read);
uint64_t submitted_count = count - marker_count;
// If the number of packets is greater than the wrapped queue size, then we
// can never submit them all at once. So submit what will fit, leaving one
// slot free for the retry barrier packet if it is not already on the
// queue.
if (submitted_count >= wrapped->amd_queue_.hsa_queue.size) {
submitted_count = free_slots - (pending_retry_point ? 0 : 1);
}
// Prefer to either submit all the packets, or none of the packets. This
// ensures that all the packets of a rewrite will be on the queue at the
// same time. This may be desirable for some rewrites. So if out of space
// defer packet insertion. Always make sure there is a free slot available
// for the retry barrier packet if there is not already one present.
else if (free_slots < submitted_count + (pending_retry_point ? 0 : 1)) {
submitted_count = 0;
}
// If we are not submitting all the packets, we need to ensure there is a
// retry packet to cause the remaining packets to be submitted. If there is
// not already a pending retry point add one.
if (submitted_count < (count - marker_count) && !pending_retry_point) {
// Reserve one slot for the barrier packet. There will always be at least
// one free slot.
assert(free_slots >= 1 &&
"Packet intercept error: there is no free slot for a retry barrier packet.\n");
// Reserve a slot for the barrier packet.
uint64_t barrier = wrapped->AddWriteIndexRelaxed(1);
assert(barrier == write &&
"Packet intercept error: wrapped queue has been updated by another thread.\n");
++write;
// Submit barrier which will wake async queue processing.
ring[barrier & mask].packet.body = {};
ring[barrier & mask].barrier_and.completion_signal = Signal::Convert(async_doorbell_);
if (Runtime::runtime_singleton_->flag().dev_mem_queue() && !needsPcieOrdering()) {
// Ensure the packet body is written as header may get reordered when writing over PCIE
_mm_sfence();
}
atomic::Store(&ring[barrier & mask].barrier_and.header, kBarrierHeader,
std::memory_order_release);
// Update the wrapped queue's doorbell so it knows there is a new packet in the queue.
HSA::hsa_signal_store_screlease(wrapped->amd_queue_.hsa_queue.doorbell_signal, barrier);
// Record the retry point
retry_index_ = barrier;
}
// Attempt to reserve useable queue space if some packets need to be
// submitted.
uint64_t new_write = submitted_count == 0
? write
: wrapped->CasWriteIndexRelaxed(write, write + submitted_count);
if (new_write == write) {
uint64_t packets_index = 0;
uint64_t write_index = 0;
uint64_t first_written_packet_index;
while (submitted_count > 0 || (packets_index < count && IsInterceptMarkerPacket(&packets[packets_index]))) {
// Ensure the marker packet callback is invoked before following
// packets are made available for the packet processor.
if (IsInterceptMarkerPacket(&packets[packets_index])) {
const amd_aql_intercept_marker_t* marker_packet =
reinterpret_cast<const amd_aql_intercept_marker_t*>(&packets[packets_index]);
marker_packet->callback(marker_packet, &wrapped->amd_queue_.hsa_queue,
write + write_index);
} else {
if (write_index == 0) {
// Leave the header of the first packet as INVALID so packet
// processor will not start processing any packets until all have
// been written and the first packet header atomically store
// released.
ring[(write + write_index) & mask].packet.body = packets[packets_index].packet.body;
first_written_packet_index = packets_index;
} else {
ring[(write + write_index) & mask] = packets[packets_index];
}
++write_index;
--submitted_count;
}
++packets_index;
}
if (write_index != 0) {
if (Runtime::runtime_singleton_->flag().dev_mem_queue() && !needsPcieOrdering()) {
// Ensure the packet body is written as header may get reordered when writing over PCIE
_mm_sfence();
}
atomic::Store(&ring[write & mask].packet.header, packets[first_written_packet_index].packet.header,
std::memory_order_release);
HSA::hsa_signal_store_screlease(wrapped->amd_queue_.hsa_queue.doorbell_signal,
write + write_index - 1);
}
return packets_index;
}
}
}
void InterceptQueue::StoreRelaxed(hsa_signal_value_t value) {
if (!active_) return;
// If called recursively defer to async doorbell thread.
if (Cursor.queue != nullptr) {
debug_print("Likely incorrect queue use observed in an interceptor.\n");
async_doorbell_->StoreRelaxed(value);
return;
}
ScopedAcquire<KernelMutex> lock(&lock_);
// Submit overflow packets.
if (!overflow_.empty()) {
uint64_t submitted_count = Submit(&overflow_[0], overflow_.size());
if (submitted_count < overflow_.size()) {
overflow_.erase(overflow_.begin(), overflow_.begin() + submitted_count);
// Since there was no space to submit all the overflow packets, there is
// no space for other packets either.
return;
}
// All overflow packets have been submitted.
overflow_.clear();
}
Cursor.queue = this;
AqlPacket* ring = reinterpret_cast<AqlPacket*>(amd_queue_.hsa_queue.base_address);
uint64_t mask = wrapped->amd_queue_.hsa_queue.size - 1;
// Loop over valid packets and process.
uint64_t end = LoadWriteIndexAcquire();
// Can only process packets that are occupying slots in the queue buffer. No
// need to add a barrier packet to ensure the extra packets are processed as
// the producer must ring the doorbell once the extra packets are made valid.
if (end > next_packet_ + amd_queue_.hsa_queue.size)
end = next_packet_ + amd_queue_.hsa_queue.size;
uint64_t i = next_packet_;
while (i < end) {
// Load the packet header as atomic acquire as it may have been written by
// another thread as atomic release. This ensures the rest of the packet
// fields are visible. Once loaded and proven not to be INVALID, further
// loads by this thread can be non-atomic.
uint16_t header = atomic::Load(&ring[i & mask].packet.header, std::memory_order_acquire);
if (!AqlPacket::IsValid(header)) break;
// Process callbacks.
Cursor.interceptor_index = interceptors.size() - 1;
Cursor.pkt_index = i;
auto& handler = interceptors[Cursor.interceptor_index];
handler.first(&ring[i & mask], 1, i, handler.second, PacketWriter);
if (Runtime::runtime_singleton_->flag().dev_mem_queue() && !needsPcieOrdering()) {
// Ensure the packet body is written as header may get reordered when writing over PCIE
_mm_sfence();
}
// Invalidate consumed packet.
atomic::Store(&ring[i & mask].packet.header, kInvalidHeader, std::memory_order_release);
// Packet has now been processed so advance the read index.
++i;
// Only allow the rewrite of one packet to be on the overflow queue. When
// packets are put on the overflow queue a barrier packet will also be
// added which has an async handler that will ring the doorbell, That
// doorbell ring will ensure this function is re-invoked to put the
// overflow packets on the hardware queue and continue rewriting packets on
// the intercept queue.
if (!overflow_.empty()) break;
}
next_packet_ = i;
Cursor.queue = nullptr;
atomic::Store(&amd_queue_.read_dispatch_id, next_packet_, std::memory_order_release);
}
hsa_status_t InterceptQueue::GetInfo(hsa_queue_info_attribute_t attribute, void* value) {
switch (attribute) {
case HSA_AMD_QUEUE_INFO_AGENT:
case HSA_AMD_QUEUE_INFO_DOORBELL_ID: {
if (!AMD::AqlQueue::IsType(wrapped.get())) return HSA_STATUS_ERROR_INVALID_QUEUE;
AMD::AqlQueue* aqlQueue = static_cast<AMD::AqlQueue*>(wrapped.get());
return aqlQueue->GetInfo(attribute, value);
}
}
return HSA_STATUS_ERROR_INVALID_ARGUMENT;
}
} // namespace core
} // namespace rocr