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

331 lines
14 KiB
Markdown

# 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
```c
#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.