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
331 lines
14 KiB
Markdown
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.
|