Files
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

14 KiB

AMDGPU Virtual Machine Design

An AMDGPU virtual machine built on the simdojo simulation framework. Models a complete SoC hierarchy, from command processors and shader engines down to compute units, wavefronts, and register files. Runs within simdojo's unified PDES epoch loop, supporting both interactive stepping and continuous execution.

File Overview

File Purpose
soc.h/cpp SoC: top-level topology root, owns XCDs, IODs, and shared GPU memory
driver.h/cpp Driver: KMD interface, lifecycle state machine
amdgpu/iod.h/cpp IOD: I/O die with memory-side cache and HBM controllers
amdgpu/memory_side_cache.h/cpp Memory-side cache component between L2 and HBM
amdgpu/hbm_controller.h HBM memory controller wrapping GpuMemory
rj_vm.cpp C API: create, step, run, checkpoint
amdgpu/command_processor.h/cpp CP: dispatch packets, doorbell loop
amdgpu/compute_unit.h/cpp CU: wavefront slots, register files, execution
amdgpu/shader_engine.h/cpp SE: container of compute units
amdgpu/xcd.h/cpp XCD: CP + shader engines
amdgpu/wavefront.h/cpp Wavefront: ISA-specific thread state
amdgpu/gpu_memory.h GpuMemory: flat address space wrapper

Component Hierarchy

SimulationEngine                       (simdojo - owns topology)
└── Topology
    └── SoC ("gpu_soc")               (CompositeComponent - topology root)
        ├── Driver driver_             (value member, not a child component)
        ├── GpuMemory ("memory")       (shared across all XCDs)
        ├── Iod[0..I] ("iod0"..)      (CompositeComponent - memory-side cache + HBM controllers)
        └── Xcd[0..N] ("xcd0"..)      (CompositeComponent)
            ├── CommandProcessor ("cp") (Component - event-driven dispatch)
            └── ShaderEngine[0..M]     (CompositeComponent)
                └── ComputeUnit[0..K]  (CompositeComponent - register files + wavefront slots)

The SoC is a simdojo::CompositeComponent set as the topology root. The simulation infrastructure (engine, topology, partitioning) is managed by SimulationEngine; the SoC represents the hardware being modeled.


Driver

The Driver models the kernel-mode driver (KMD) interface presented to a user-mode driver (e.g., rocr). It is owned as a value member of SoC (Driver driver_{*this}) - the driver is part of the hardware, not external software.

The Driver is a thin bridge between the application and the simulation engine. It exposes two methods:

  • submit(DispatchPacket, uint32_t xcd_idx) - enqueues a dispatch packet on the target XCD's command processor doorbell queue.
  • close() - shuts down the simulation by calling engine()->request_exit().

Command Processor

The CP reads dispatch packets and distributes wavefronts across registered compute units in round-robin order.

Event-Driven Dispatch

The CP is event-driven. During startup(), it schedules a doorbell event for any pre-loaded packets. Each doorbell event triggers step(), which processes one dispatch packet: for each workgroup, it calls dispatch_wf() on the next CU in round-robin order. dispatch_wf() self-schedules the CU's tick via schedule_work(); there is no separate activate() call.

  • enqueue(packet) - pre-loads packets without triggering dispatch (used by config loader and tests)
  • submit(packet) - thread-safe; appends packet and schedules an async doorbell event via schedule_event_now()

Each CU runs its dispatched wavefronts independently. The CU is self-driving: dispatch_wf() calls schedule_work(), which schedules a tick event (only when the CU has runnable wavefronts) that calls execute_quantum(). A quantum executes up to kFunctionalQuantum instructions, but may yield early when a wavefront requests it (e.g. s_sleep, a vendor-dependency retry). The tick reschedules itself at now + max(1, last_quantum_executed_) — i.e. by the work actually executed, so an early yield resumes promptly instead of leaping a full quantum, while max(1, ...) keeps the event strictly in the future. Since functional mode is 1 CPI, ticks advance proportionally to instruction count. The quantum allows CU events to interleave, guaranteeing forward progress for inter-CU synchronization patterns such as spin-locks or semaphore acquire/release on global memory.

A wavefront that reaches s_endpgm halts: it frees its SGPR/VGPR resources immediately and notifies the CP of workgroup completion (there is no separate lazy retirement pass). When a CU has no resident wavefronts it stops scheduling and fires its on_idle callback. When all CUs are idle and no packets remain, the CP signals completion via engine()->primary_release().


Dispatch Packet Flow

Config loader / Driver::submit()
  └── cp->enqueue(packet)  or  cp->submit(packet) [+ async doorbell event]
        └── cp->step() processes one packet:
              for each workgroup:
                cu = next CU (round-robin)
                cu->dispatch_wf(wg_id, pc, sgprs, vgprs)
                  └── find idle slot, allocate SGPR/VGPR blocks
                      initialize wavefront state (pc, wg_id)
                      schedule_work() -> tick event (self-driving)

A DispatchPacket specifies:

  • kernel_entry_pc - byte address of kernel code in GPU memory
  • workgroup_count - number of workgroups to launch
  • wfs_per_workgroup - wavefronts per workgroup
  • sgprs_per_wf / vgprs_per_wf - register requirements (from code object)

Wavefronts are distributed round-robin across CUs within the XCD. Each CU allocates a contiguous block in its physical SGPR and VGPR files for the wavefront.


Memory Hierarchy and Coherence

Each CU has private L1 scalar (K$) and L1 vector (V$) caches backed by a shared L2 per XCD. The memory type (Mtype), derived from instruction encoding bits (sc0/sc1/nt), controls caching behavior:

Mtype sc1 sc0 L1 Behavior L2 Behavior
RW 0 0 Cached, write-through Write-back
CC 0 1 Invalidate-on-read, write-through Write-through to HBM
UC 1 0 Bypass Bypass
NT 0 0+nt Bypass L1 Cached

CC (coherently cacheable) loads invalidate the L1 line before refetching from L2, matching real SC0/GLC hardware behavior. This ensures that stores from other CUs (which write through L1 to L2) are visible to polling loops.

Atomic operations (flat_atomic_*) bypass L1 entirely and perform read-modify-write at L2. The old value is returned to vdst when SC0/GLC is set. The L1 line is invalidated after the atomic to prevent stale reads. Supported integer atomics: swap, cmpswap, add, sub, smin/umin, smax/umax, and, or, xor, inc, dec.

Cache management instructions:

  • s_dcache_inv / s_dcache_inv_vol — invalidate the L1 scalar cache
  • s_gl1_inv — invalidate the L1 vector cache

Execution Modes

Interactive Stepping (rj_vm_step)

Synchronous, single-threaded. The caller drives execution one tick at a time:

rj_vm_step(vm, &active)
  └── engine.step()         process all events at next timestamp
        └── CP doorbell event fires → cp->step() drains dispatch queue
            CU tick events fire → execute one instruction per wavefront

Returns active=1 while any wavefront is still executing. The engine is built during rj_vm_create(), not on first step.

Continuous Execution (rj_vm_run)

The simulation thread runs engine.run(), which drains the event queue continuously. The main thread injects work via schedule_event_async():

Main thread                          Simulation thread
───────────                          ─────────────────
                                     engine.run()
                                       └── epoch loop processes events
                                             │
driver().submit(packet)  ──────────►  async event → CP doorbell fires
  │                                          │
  │                                     cp->step() dispatches wavefronts
  │                                     CU tick events execute instructions
  │                                          │
driver().close()         ──────────►  done_ set, workers stop
  │                                        (close() calls engine()->request_exit())
  │
sim_thread.join()  ◄─────────────────────    engine shuts down components
  │
engine.shutdown()

In single-threaded mode, the engine drains events in timestamp order without LBTS synchronization. The CP schedules doorbell events during startup() for pre-loaded packets and via schedule_event_async() for external submissions.


Simulation Integration

The SoC plugs into simdojo's SimulationEngine directly. The C API layer (rj_vm.cpp) owns the engine and wires the SoC into the topology:

  1. Construction - The config loader parses a declarative JSON topology config and creates a SoC with engine configuration. The C API sets the SoC as the topology root via engine.topology().set_root(std::move(soc)).

  2. create() - Partitions topology, initializes all components, and sets up the engine.

  3. run() - Starts all components and drains events continuously. In single-threaded mode, events are processed in timestamp order until the queue empties and termination conditions are met. In multi-threaded mode, workers run LBTS-synchronized epoch loops.

  4. step() - Processes all events at the next timestamp (one tick step). Returns whether the simulation can continue.

  5. shutdown() - Calls shutdown() on all components, tears down engine.

After run() returns, last_exit() provides a ExitStatus with the termination reason (COMPLETED, EXIT_REQUEST, INTERRUPTED), the simulation tick, and a human-readable message. Components can call engine()->request_exit(reason, code) to stop the simulation.


KMD Emulation (kmd/)

The kmd/linux/ layer makes a real ROCm stack (ROCR + libhsakmt) run against the simulated GPU without any kernel driver. It is Linux-only and activated via LD_PRELOAD.

Architecture

ROCm application
  └── ROCR / HIP runtime
        └── libhsakmt
              ├── open("/dev/kfd")    ──►  interposer.cpp intercepts
              ├── ioctl(kfd_fd, …)   ──►  SimulatedKfd::ioctl()
              ├── mmap(kfd_fd, …)    ──►  SimulatedKfd::mmap()
              ├── fopen("/sys/…")    ──►  interposer.cpp redirects → Sysfs temp dir
              └── close(kfd_fd)      ──►  SimulatedKfd::close()

Components

File Purpose
interposer.cpp LD_PRELOAD shim: intercepts open, ioctl, mmap, munmap, fopen, close via syscall
simulated_kfd.h/cpp SimulatedKfd: handles all KFD ioctls, owns doorbell/event pages
sysfs.h/cpp Sysfs: generates a per-process /tmp/rocjitsu_topology_* directory that ROCR reads instead of the real /sys/devices/virtual/kfd/kfd/topology

KFD ioctl surface

ioctl Handler Notes
GET_VERSION get_version_ioctl Returns KFD_IOCTL_MAJOR/MINOR_VERSION
GET_PROCESS_APERTURES_NEW get_apertures_ioctl Returns default_apertures_ with per-instance gpu_id
ACQUIRE_VM acquire_vm_ioctl No-op (VM is always acquired)
ALLOC_MEMORY_OF_GPU alloc_memory_ioctl Allocates host memory, assigns GPU VA from a linear bump allocator
FREE_MEMORY_OF_GPU free_memory_ioctl Frees host memory, removes VA mapping
MAP_MEMORY_TO_GPU / UNMAP map/unmap ioctls No-op (host pointers serve as GPU VAs)
CREATE_QUEUE create_queue_ioctl Registers an AQL ring with the CP; deferred until doorbell page is mapped
DESTROY_QUEUE destroy_queue_ioctl Unregisters the ring from the CP
CREATE_EVENT create_event_ioctl Allocates a slot in the memfd-backed signal page
DESTROY_EVENT destroy_event_ioctl Removes event slot; wakes any WAIT_EVENTS callers
SET_EVENT set_event_ioctl Marks slot non-zero with memory_order_release; notifies waiters
WAIT_EVENTS wait_events_ioctl Waits up to 100ms then returns; simulates wake_up_interruptible

Signal event page

libhsakmt expects a memfd-backed page at the KFD mmap offset KFD_MMAP_TYPE_EVENTS | gpu_id. Each 64-bit slot corresponds to one event_id. libhsakmt polls signal_page[event_id] directly; non-zero means the event is pending. close() sets all slots to 1 to unblock any polling threads during shutdown.


C API (rj_vm.h)

The C API wraps the VM behind an opaque rj_vm_t handle:

rj_vm_t is reference-counted (extends RefCounted). Use rj_vm_retain and rj_vm_release to manage shared ownership; rj_vm_destroy is a convenience wrapper that releases the last reference and tears down the VM.

Function Description
rj_vm_create() Load config from JSON file, build VM and engine
rj_vm_create_from_string() Load config from JSON string
rj_vm_retain() Increment reference count
rj_vm_release() Decrement reference count; destroys when it reaches zero
rj_vm_destroy() Tear down VM
rj_vm_step() One interactive step
rj_vm_run() Run to completion via driver open/close
rj_vm_save_checkpoint() Serialize VM state to FlatBuffer
rj_vm_restore_checkpoint() Restore VM from checkpoint file

Example

#include <rocjitsu/rocjitsu.h>

rj_vm_t *vm = NULL;
rj_vm_create("configs/gfx950_cdna4.json", &vm);

uint64_t ticks = 0;
rj_vm_run(vm, &ticks);

rj_vm_destroy(vm);

Internal C++ code (tests, GUI) accesses the SoC directly via the config loader (config::load_config() / config::load_config_from_string()). The rj_vm.h header is a pure C API with opaque handles.