Skip to content
Merged
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
6 changes: 6 additions & 0 deletions emulation/rocjitsu/lib/python/amdisa/codegen/_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1924,6 +1924,12 @@ def gen_execution_backend(self) -> None:
f' &execute_with_backend<{class_name}>,\n'
for class_name in self._split_execution_classes
)
# Generated executors follow Instruction's decoded-reuse contract:
# non-memory instructions without DynamicInstState can execute again
# on another wave. Read values and EXEC from the current context,
# restore temporary operand delegates, and assign per-execution flags
# on every call. Persistent per-issue state belongs in DynamicInstState,
# not decoded encoding or operand members.
source = textwrap.dedent(f'''\
{CppFile._prologue_comment()}
#include "{generated_arch}/execution_backend.h"
Expand Down
9 changes: 5 additions & 4 deletions emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/isa/decoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ struct IsaExecutionBackend;
/// By default, decoded instructions are heap-allocated. Call
/// ``enable_pool()`` to route Instruction::operator new/delete through
/// the decoder's O(1) free-list pool. Only enable the pool when all
/// decoded instructions will be deleted before the decoder is destroyed
/// (e.g., the ComputeUnit simulation loop).
/// decoded instructions will be deleted on the bound thread before the decoder
/// is destroyed. CU execution uses heap storage because decoded instructions
/// can survive issue quanta and move between workers.
class Decoder {
public:
using Pool = util::ArenaAlloc<512, 128>;
Expand Down Expand Up @@ -97,8 +98,8 @@ class Decoder {
///
/// When active, Instruction::operator new/delete route through the
/// decoder's pool for O(1) alloc/free. Only enable when the caller
/// guarantees all instructions will be deleted before the decoder
/// is destroyed (e.g., the ComputeUnit hot path).
/// guarantees all instructions will be deleted on the bound thread
/// before the decoder is destroyed. CU execution uses heap storage.
void enable_pool() {
activate_pool([](void *p, size_t s) -> void * { return static_cast<Pool *>(p)->allocate(s); },
[](void *p, void *ptr) { static_cast<Pool *>(p)->deallocate(ptr); }, &pool_);
Expand Down
14 changes: 11 additions & 3 deletions emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/isa/instruction.h
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ class Instruction : public util::IListNode<Instruction, util::IListParent<BasicB
virtual ~Instruction() = default;

/// @brief Pool allocator hooks, set by the decoder's enable_pool().
/// Thread-local because each CU partition thread has its own decoder/pool.
/// Instructions are wholly owned by their CU and always allocated/freed
/// on the same thread.
/// @details Pool users must allocate and free on the bound thread, with the
/// decoder outliving its pooled instructions. CU execution instead forces
/// heap allocation so instructions can survive quanta and worker migration.
using AllocFn = void *(*)(void *pool, size_t size);
using DeallocFn = void (*)(void *pool, void *ptr);
static thread_local inline AllocFn alloc_fn_;
Expand Down Expand Up @@ -204,6 +204,13 @@ class Instruction : public util::IListNode<Instruction, util::IListParent<BasicB
/// and must not be called. No virtual dispatch.
/// This is a low-level backend callback. AMDGPU callers should use the CU's
/// execute_instruction() API to reset and check simulator execution failures.
/// @details Decoded non-memory instructions without DynamicInstState may be
/// reused across waves without a reset. Executors must read register values,
/// EXEC and other execution state from the current context, and restore any
/// temporary operand delegates before returning. Put persistent per-issue
/// state in DynamicInstState; its presence excludes decoded reuse. Any
/// per-execution member flags must be assigned on every execution, as with
/// set_memory_wait_result_written().
const ExecuteFn execute;

/// @brief Access the attached dynamic state, or nullptr if none.
Expand All @@ -226,6 +233,7 @@ class Instruction : public util::IListNode<Instruction, util::IListParent<BasicB
}

/// @brief Attach dynamic state to this instruction (transfers ownership).
/// @details An instruction retaining this state cannot enter a decoded cache.
/// @param[in] d Dynamic state (ownership transferred).
void set_data(std::unique_ptr<DynamicInstState> d) { data_ = std::move(d); }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ class AsyncInstructionQueue {
};

/// @brief A bounded same-wave execution window.
/// @details Draining before CU rescheduling keeps decoder pools, wave storage
/// and data caches owned by the issuing thread.
/// @details Draining before CU rescheduling keeps wave storage and data caches
/// stable until asynchronous work finishes.
class AsyncInstructionWindow {
using Access = async_execution::Access;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,9 @@ ComputeUnitCore::ComputeUnitCore(std::string name, const Config &config, GpuMemo

inst_cache_.set_l2(l2_);

// Enable pool allocation for the hot decode-execute path.
// Instructions decoded during step() are always deleted before the CU
// (and its decoder) are destroyed, so pool allocation is safe here.
decoder_->enable_pool();
// Decoded instructions use heap storage: cache entries and asynchronous
// work can outlive an issue quantum and move between execution workers.
// Do not install this decoder's pool on the constructing thread.

wfs_.resize(config.num_wf_slots);
sgpr_file_.init(config.num_wf_slots * config.sgprs_per_wf, config.sgprs_per_wf);
Expand Down Expand Up @@ -1510,7 +1509,9 @@ template <bool EnableAsync>
active->trace_inst_count_++;

util::StringDiagnostic decode_error;
DecodeResult decoded = decoder_->decode(words, decode_error.emitter());
const bool reuse_decoded = !debug_active();
DecodeResult decoded = decoded_inst_cache_.decode(*decoder_, active->pc, vmid, words,
decode_error.emitter(), reuse_decoded);
if (decoded.failed()) {
drain_async_window();
util::Logger::vm("CU ", this->name(), ": wf", active->wf_id(), " HALT(decode rejection) pc=0x",
Expand All @@ -1526,6 +1527,8 @@ template <bool EnableAsync>
return;
}
Instruction *inst = decoded.value().get();
DecodedInstructionCache::ScopedReturn return_decoded(decoded_inst_cache_, decoded.value(),
active->pc, vmid, words, reuse_decoded);

int inst_size_signed = inst->size();
assert(inst_size_signed > 0 && "instruction size must be positive");
Expand Down Expand Up @@ -1726,6 +1729,7 @@ template <bool EnableAsync>
}();

if (execution_result.failed()) [[unlikely]] {
return_decoded.discard();
if constexpr (EnableAsync) {
if (window)
window->drain();
Expand All @@ -1748,7 +1752,7 @@ template <bool EnableAsync>
// pc, and allocations are now zeroed, so the after-execute hook, result logging,
// and pc-advance below must not run on the dead slot. The dedicated
// onAmdgpuWavefrontHalted hook already fired (with live state) from halt().
// s_endpgm is never a memory op, so just reclaim the decoded instruction.
// The completed s_endpgm can return to the decoded cache on scope exit.
//
// Note the intentional asymmetry: an s_endpgm that defers to ENDING (pending
// memory waits) is NOT halted here, so it DOES fire onAmdgpuAfterExecuteInstruction
Expand Down Expand Up @@ -1930,8 +1934,6 @@ template <bool EnableAsync>
return;
}
}
} else {
decoded.value().reset();
}

active->pc += inst_size;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "rocjitsu/isa/decoder.h"
#include "rocjitsu/isa/instruction.h"
#include "rocjitsu/vm/amdgpu/cluster_lds_multicast.h"
#include "rocjitsu/vm/amdgpu/decoded_instruction_cache.h"
#include "rocjitsu/vm/amdgpu/gpu_memory.h"
#include "rocjitsu/vm/amdgpu/gpu_vm.h"
#include "rocjitsu/vm/amdgpu/instruction_cache.h"
Expand Down Expand Up @@ -1056,7 +1057,7 @@ class RJ_API_TYPE_EXPORT ComputeUnitCore : public simdojo::CompositeComponent {
void replace_decoder_for_test(std::unique_ptr<Decoder> decoder) {
assert(decoder != nullptr);
assert(!has_active_wfs());
decoder->enable_pool();
decoded_inst_cache_.clear();
decoder_ = std::move(decoder);
}

Expand Down Expand Up @@ -1291,6 +1292,7 @@ class RJ_API_TYPE_EXPORT ComputeUnitCore : public simdojo::CompositeComponent {
L1ScalarCache l1_scalar_;
L1VectorCache l1_vector_;
InstructionCache inst_cache_;
DecodedInstructionCache decoded_inst_cache_;
/// @brief Debug attach/detach transitions seen by set_debug_active().
std::atomic<uint64_t> inst_cache_debug_epoch_{0};
/// @brief The epoch this CU's thread has already invalidated the I$ for.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) 2026 Advanced Micro Devices, Inc.
// SPDX-License-Identifier: MIT

#pragma once

#include "rocjitsu/isa/decoder.h"
#include "rocjitsu/isa/instruction.h"

#include <algorithm>
#include <array>
#include <cstdint>
#include <exception>
#include <memory>
#include <span>

namespace rocjitsu::amdgpu {

/// @brief Reuse completed instructions on one CU's issue path.
///
/// @details Fetchability and the byte I$ remain authoritative: lookups compare
/// all fetched words, including literals, so code maintenance and debugger
/// writes cannot reuse stale decodes.
/// Instructions that borrow the fetch buffer cannot be retained. Entries use
/// heap storage because a CU can run on different workers. Re-execution follows
/// the stateless Instruction::execute contract.
class DecodedInstructionCache {
public:
// Bounded host-speed choice; direct indexing requires a power of two.
static constexpr size_t kNumEntries = 256;
static_assert(kNumEntries != 0 && (kNumEntries & (kNumEntries - 1)) == 0);
// Match the four-word CU instruction-fetch window, including literals.
static constexpr size_t kNumWords = 4;
using Words = std::span<const uint32_t, kNumWords>;

~DecodedInstructionCache() { clear(); }

/// @brief Decode from freshly fetched words, or take the matching cached object.
///
/// @details The CU uses heap storage for every miss, including uncached
/// memory operations and debugger execution. The allocation guard also
/// bypasses an unrelated decoder pool installed by the caller.
/// @param enabled False bypasses reuse and table allocation while debugging.
DecodeResult decode(Decoder &decoder, uint64_t pc, uint32_t vmid, Words words,
const DecodeErrorEmitter &emit_error, bool enabled = true) {
if (enabled && !entries_)
entries_ = std::make_unique<std::array<Entry, kNumEntries>>();
if (enabled && entries_) {
auto &entry = (*entries_)[index(pc)];
if (entry.instruction && entry.pc == pc && entry.vmid == vmid &&
std::equal(words.begin(), words.end(), entry.words.begin()))
return std::move(entry.instruction);
}
Instruction::ScopedHeapAllocation heap_allocation;
return decoder.decode(words.data(), emit_error);
}

/// @brief Return only completed, stateless instructions.
///
/// @details Construct immediately after decode() with the same key and policy.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be safer if decode() returned an RAII lease capturing the key, reuse policy, and original fetch span, with access to the owned instruction for pipeline handoff.

The CU passes the same arguments correctly today (compute_unit.cpp:1512–1531), but ScopedReturn accepts them independently. In particular, a byte-identical copy of words satisfies the documented “same key” requirement while defeating borrows_fetch_buffer(): a VOPD instruction still points into the original decode buffer, which the copied span does not cover. A mismatched PC or policy can likewise return an object under the wrong key.

Capturing these inputs once would preserve the existing move/release and failed-execution paths without leaving this lifetime contract to every caller. This is an API safety suggestion; the current CU call site uses the correct span.

/// Memory pipelines and async execution take ownership by releasing/moving
/// the caller's unique_ptr, so their in-flight instructions cannot become
/// cache entries.
class ScopedReturn {
public:
ScopedReturn(DecodedInstructionCache &cache, std::unique_ptr<Instruction> &instruction,
uint64_t pc, uint32_t vmid, Words words, bool enabled = true)
: cache_(cache), instruction_(instruction), pc_(pc), vmid_(vmid), words_(words),
enabled_(enabled), exceptions_(std::uncaught_exceptions()) {}

~ScopedReturn() {
if (enabled_ && instruction_ && !instruction_->is_memory_op() && !instruction_->data() &&
!borrows_fetch_buffer(*instruction_, words_) && std::uncaught_exceptions() == exceptions_)
cache_.put(pc_, vmid_, words_, instruction_);
}

ScopedReturn(const ScopedReturn &) = delete;
ScopedReturn &operator=(const ScopedReturn &) = delete;

/// @brief Exclude a failed execution from reuse.
void discard() { enabled_ = false; }

private:
DecodedInstructionCache &cache_;
std::unique_ptr<Instruction> &instruction_;
uint64_t pc_;
uint32_t vmid_;
Words words_;
bool enabled_;
int exceptions_;
};

/// @brief Drop entries before replacing the decoder/target, or destroying this CU.
void clear() {
Instruction::ScopedHeapAllocation heap_allocation;
entries_.reset();
}

private:
struct Entry {
uint64_t pc = 0;
uint32_t vmid = 0;
std::array<uint32_t, kNumWords> words{};
std::unique_ptr<Instruction> instruction;
};

static size_t index(uint64_t pc) { return (pc / sizeof(uint32_t)) & (kNumEntries - 1); }

static bool borrows_fetch_buffer(const Instruction &instruction, Words words) {
const auto raw = reinterpret_cast<uintptr_t>(instruction.raw_encoding());
const auto begin = reinterpret_cast<uintptr_t>(words.data());
return raw >= begin && raw - begin < words.size_bytes();
}

void put(uint64_t pc, uint32_t vmid, Words words, std::unique_ptr<Instruction> &instruction) {
// A decode miss allocates the table before execution; returning an
// instruction during stack unwinding must not allocate.
if (!entries_)
return;
auto &entry = (*entries_)[index(pc)];
if (entry.instruction) {
Instruction::ScopedHeapAllocation heap_allocation;
entry.instruction.reset();
}
entry.pc = pc;
entry.vmid = vmid;
std::copy(words.begin(), words.end(), entry.words.begin());
entry.instruction = std::move(instruction);
}

std::unique_ptr<std::array<Entry, kNumEntries>> entries_;
};

} // namespace rocjitsu::amdgpu
Loading
Loading