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
295 lines
11 KiB
Markdown
295 lines
11 KiB
Markdown
# Rocjitsu CLI and Transport Layer Design
|
|
|
|
## Overview
|
|
|
|
The `rocjitsu` CLI is the primary entry point for running applications on
|
|
the simulated GPU. It operates in two modes:
|
|
|
|
- **Local mode**: Sets `LD_PRELOAD` and `execve`s the target application.
|
|
The simulator runs in-process via the interposer.
|
|
- **Daemon mode**: Spawns a daemon child process that hosts the simulation
|
|
engine, then `LD_PRELOAD` + `execve`s the target. Client processes
|
|
communicate with the daemon via RPC over a Unix domain socket. GPU memory
|
|
is shared using memfds passed via SCM_RIGHTS.
|
|
|
|
Daemon mode supports vLLM's multiprocessing spawn, torchrun, torch.distributed,
|
|
and NCCL --- workloads where multiple processes share a single simulated GPU.
|
|
|
|
## Command-Line Options
|
|
|
|
```
|
|
Usage: rocjitsu --config <config.json> [--daemon|--attach] -- <app> [args...]
|
|
```
|
|
|
|
| Option | Description |
|
|
|--------|-------------|
|
|
| `--config <path>` | Path to simulation config JSON (required) |
|
|
| `--daemon` | Run in daemon mode: fork a daemon process hosting the simulation engine, then launch the application with the interposer. Without `-- <app>`, runs the daemon server only. |
|
|
| `--attach` | Attach to a running daemon. The socket path is resolved as `$ROCJITSU_RUNTIME_DIR/daemon.sock`, then `$XDG_RUNTIME_DIR/rocjitsu/daemon.sock`, falling back to `/tmp/rocjitsu-<uid>/daemon.sock`. |
|
|
| `--help`, `-h` | Print usage and exit |
|
|
| `--version`, `-v` | Print version and exit |
|
|
| `--` | Separator between rocjitsu options and the target application command line |
|
|
|
|
### Usage Examples
|
|
|
|
```bash
|
|
# Local mode: in-process simulation
|
|
rocjitsu --config configs/gfx950_cdna4_kmd.json -- ./app
|
|
|
|
# Daemon mode: fork daemon + launch app
|
|
rocjitsu --daemon --config configs/gfx950_cdna4_kmd.json -- ./app args...
|
|
|
|
# Daemon-only: run server (no app launched)
|
|
rocjitsu --daemon --config configs/gfx950_cdna4_kmd.json
|
|
|
|
# Attach to running daemon
|
|
rocjitsu --attach --config configs/gfx950_cdna4_kmd.json -- ./app
|
|
```
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Client Process (HIP application) Daemon Process
|
|
+-----------------------------------+ +-----------------------------------+
|
|
| HIP Runtime | | SimulationEngine (background) |
|
|
| ROCR (HSA Runtime) | | VirtualMachine (topology root) |
|
|
| libhsakmt (KFD thunk) | | SoC -> XCD -> CP, CUs, L2 |
|
|
| | | | SimulatedDriver (KFD ioctls) |
|
|
| v | | EventState, allocations |
|
|
| LD_PRELOAD interposer | +-----------------------------------+
|
|
| |-- open("/dev/kfd") | |
|
|
| | RemoteDriver (RPC stub) --+----- Unix socket --+
|
|
| |-- ioctl(kfd_fd, ...) | (RPC_IOCTL)
|
|
| |-- mmap(drm_fd, offset) | (RPC_MMAP + SCM_RIGHTS)
|
|
| |-- close(kfd_fd) | (RPC_CLOSE)
|
|
| `-- madvise/mprotect/munmap |
|
|
+-----------------------------------+
|
|
```
|
|
|
|
### Ownership Chain
|
|
|
|
```
|
|
rj_vm_t (C API handle)
|
|
|-- engine (unique_ptr<SimulationEngine>)
|
|
| `-- topology
|
|
| `-- root (unique_ptr<VirtualMachine>)
|
|
| |-- SoC (adopted children: XCD, GpuMemory, ...)
|
|
| `-- driver_ (unique_ptr<SimulatedDriver>)
|
|
`-- engine_config
|
|
```
|
|
|
|
No circular ownership. Destroying `rj_vm_t` destroys the engine, which
|
|
destroys the topology root (VirtualMachine), which destroys the driver.
|
|
|
|
### Process Modes
|
|
|
|
| Mode | Entry Point | Driver | Engine |
|
|
|------|------------|--------|--------|
|
|
| Local | `rocjitsu -- ./app` | SimulatedDriver (in-process) | Background thread |
|
|
| Daemon (server) | `rocjitsu --daemon -- ./app` | SimulatedDriver (in-process) | Background thread |
|
|
| Daemon (client) | LD_PRELOAD interposer | RemoteDriver (RPC stub) | In daemon process |
|
|
|
|
## RPC Protocol
|
|
|
|
### Message Format
|
|
|
|
All messages use a fixed 16-byte header followed by an opcode-specific payload:
|
|
|
|
```
|
|
0 1 2 3
|
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
| opcode | reserved | request_id |
|
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
| payload_bytes |
|
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
| result |
|
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
```
|
|
|
|
### Opcodes
|
|
|
|
| Opcode | Direction | Payload | Memfd |
|
|
|--------|-----------|---------|-------|
|
|
| `RPC_HANDSHAKE` | C->D->C | `RpcHandshakeResponse` + topology path | No |
|
|
| `RPC_CLOSE` | C->D->C | None | No |
|
|
| `RPC_MMAP` | C->D->C | `RpcMmapRequest` / `RpcMmapResponse` | Optional |
|
|
| `RPC_MUNMAP` | C->D->C | `RpcMunmapRequest` | No |
|
|
| `RPC_IOCTL` | C->D->C | `RpcIoctlRequest` + raw args + inlined arrays | Optional |
|
|
|
|
### Embedded Pointer Serialization
|
|
|
|
KFD ioctls like `WAIT_EVENTS`, `MAP_MEMORY_TO_GPU`, and
|
|
`GET_PROCESS_APERTURES_NEW` contain pointer fields that reference inline
|
|
arrays. The client serializes these by inlining the pointed-to array after
|
|
the struct. The daemon reconstructs the pointer to its local buffer. The
|
|
client saves and restores original pointers across the RPC round-trip.
|
|
|
|
### File Descriptor Passing
|
|
|
|
Memfds are passed via `sendmsg()`/`recvmsg()` with `SCM_RIGHTS` ancillary
|
|
data. This is the standard Unix mechanism for cross-process file descriptor
|
|
sharing (same pattern as QEMU vhost-user and CRIU).
|
|
|
|
Two RPC operations carry memfds:
|
|
- `RPC_IOCTL` (ALLOC_MEMORY response) --- allocation backing memfd
|
|
- `RPC_MMAP` (response) --- doorbell and event page memfds
|
|
|
|
### Thread Safety
|
|
|
|
ROCR is heavily multithreaded. The `rpc_mutex_` in `RemoteDriver` serializes
|
|
all send+recv pairs. `WAIT_EVENTS` uses client-side polling with `ppoll` on
|
|
a shutdown `eventfd` between daemon timeout=0 polls, avoiding mutex starvation.
|
|
|
|
## Memory Sharing (Same-VA Architecture)
|
|
|
|
Both client and daemon map every memfd at the **same virtual address** using
|
|
MAP_FIXED. GPU VAs are in the GPUVM aperture (`0x1000000000` - `0x3FFFFFFFFFFF`),
|
|
which doesn't collide with either process's normal address space.
|
|
|
|
This means `reinterpret_cast<void*>(gpu_va)` works in both processes ---
|
|
AQL packets, kernarg pointers, signal handles, ring buffers, and rptr/wptr
|
|
all resolve correctly without VA translation.
|
|
|
|
### Allocation Flow
|
|
|
|
```
|
|
Client Daemon
|
|
------ ------
|
|
1. ROCR: fmm_allocate()
|
|
mmap(MAP_PRIVATE|MAP_NORESERVE)
|
|
|
|
2. ROCR: hsaKmtAllocMemory()
|
|
RemoteDriver::send_ioctl(ALLOC) ----> alloc_memory_ioctl()
|
|
memfd_create + ftruncate + fallocate
|
|
MAP_FIXED memfd at VA (proactive)
|
|
<---- response + memfd (SCM_RIGHTS)
|
|
[USERPTR: mincore+copy, MAP_FIXED]
|
|
|
|
3. ROCR: fmm_map_to_cpu()
|
|
mmap(drm_fd, MAP_FIXED, offset)
|
|
interposer routes DRM fd ----> mmap() -> already mapped, return
|
|
<---- response
|
|
```
|
|
|
|
### Proactive Mapping
|
|
|
|
The daemon MAP_FIXED maps all allocations at ALLOC_MEMORY time, not at
|
|
mmap time. This prevents a race where the CP thread dereferences kernarg
|
|
pointers before the client's fmm_map_to_cpu RPC arrives.
|
|
|
|
### USERPTR Sharing
|
|
|
|
For USERPTR allocations (ring buffer, rptr/wptr), the client receives the
|
|
daemon's memfd, copies committed pages via mincore, then MAP_FIXED replaces
|
|
the anonymous pages with the shared memfd. The interposer suppresses
|
|
MADV_HUGEPAGE and MADV_DONTFORK on GPUVM addresses to prevent kernel 6.17
|
|
shmem fault failures.
|
|
|
|
### Trust Model
|
|
|
|
The RPC protocol assumes a trusted, version-matched client (i.e., the
|
|
same rocjitsu build on both sides). The handshake rejects version
|
|
mismatches, but payload contents are not validated beyond size checks.
|
|
Untrusted or adversarial clients are out of scope.
|
|
|
|
## Transport Layer
|
|
|
|
### Abstract Interface
|
|
|
|
```cpp
|
|
class Transport {
|
|
public:
|
|
virtual bool send(const void *data, size_t len) = 0;
|
|
virtual bool recv(void *data, size_t len) = 0;
|
|
virtual ssize_t send_with_handle(const void *data, size_t len, int handle) = 0;
|
|
virtual ssize_t recv_with_handle(void *data, size_t len,
|
|
int *handle_out, size_t *num_handles) = 0;
|
|
virtual void close() = 0;
|
|
};
|
|
```
|
|
|
|
### Unix Implementation
|
|
|
|
`UnixTransport` wraps a Unix domain socket with SCM_RIGHTS for handle passing.
|
|
Static factory methods:
|
|
- `UnixTransport::listen(endpoint)` --- create a listening socket
|
|
- `UnixTransport::connect(endpoint)` --- connect to a daemon
|
|
- `transport->accept()` --- accept a client connection
|
|
|
|
The endpoint is `$XDG_RUNTIME_DIR/rocjitsu/daemon.sock` by default.
|
|
|
|
### Platform Portability
|
|
|
|
The transport abstraction enables future Windows support:
|
|
- Unix: Unix domain socket + SCM_RIGHTS
|
|
- Windows: Named pipe + DuplicateHandle
|
|
|
|
The RPC message format (header + payload) is platform-independent. Only the
|
|
transport and shared-memory handle mechanism change per platform.
|
|
|
|
## Event System
|
|
|
|
### Auto-Reset Semantics in Daemon Mode
|
|
|
|
In daemon mode, WAIT_EVENTS uses timeout=0 polling via RPC. Multiple client
|
|
threads poll the same event independently. Auto-reset is skipped on timeout=0
|
|
polls to prevent one poller from "stealing" the event signal from others.
|
|
This matches the real kernel's behavior where `wake_up_all()` wakes all
|
|
waiters simultaneously.
|
|
|
|
### Shutdown Sequence
|
|
|
|
1. ROCR calls SET_EVENT on wake signals during `AsyncEventsControl::Shutdown()`
|
|
2. All client-side pollers see the event (no auto-reset on polls)
|
|
3. ROCR's async threads exit their loops, thread joins succeed
|
|
4. ROCR calls DESTROY_QUEUE, DESTROY_EVENT, close(kfd_fd)
|
|
5. `RemoteDriver::close()` sends RPC_CLOSE to daemon
|
|
6. Daemon calls `SimulatedDriver::close()` -> `notify_closing()`
|
|
|
|
## Fd Management
|
|
|
|
### Fd Reservation (DMTCP Pattern)
|
|
|
|
The driver reserves a contiguous fd range (4000-4255) for internal memfds.
|
|
`claim_fd(real_fd)` moves a real fd into this range via `dup2`. The
|
|
interposer's `close()` checks `owns_reserved_fd(fd)` to protect internal
|
|
fds from ROCR's normal fd lifecycle.
|
|
|
|
### Fd Protection
|
|
|
|
ROCR opens and closes many fds during init (topology files, /proc, DRM
|
|
render nodes). Without protection, a transient file's fd number could
|
|
collide with an allocation memfd. The `owned_fds_` set in `SimulatedDriver`
|
|
tracks all internal memfds, and the interposer suppresses close() on
|
|
protected fds.
|
|
|
|
## Limitations
|
|
|
|
### Kernel 6.17 shmem Bug
|
|
|
|
MAP_SHARED at GPUVM addresses can trigger SIGBUS on kernel 6.17 due to a
|
|
shmem large folio regression. The interposer suppresses MADV_HUGEPAGE and
|
|
MADV_DONTFORK on GPUVM addresses as a workaround. Upstream fixes are in
|
|
kernel 6.12.59+ and 6.15+.
|
|
|
|
## File Map
|
|
|
|
| File | Role |
|
|
|------|------|
|
|
| `rpc.h` | Protocol types, send/recv helpers |
|
|
| `transport.h/.cpp` | Abstract transport + Unix socket implementation |
|
|
| `remote_driver.h/.cpp` | Client-side RPC stub |
|
|
| `simulated_kfd.h/.cpp` | KFD ioctl dispatch, allocation table, events |
|
|
| `interposer.cpp` | LD_PRELOAD syscall intercepts |
|
|
| `events.h/.cpp` | KFD event subsystem (create, set, wait, destroy) |
|
|
| `tools/rocjitsu/main.cpp` | CLI entry point, RPC dispatch loop (daemon mode) |
|
|
| `tests/daemon_test.cpp` | Gtest fixture for daemon end-to-end tests |
|
|
| `virtual_machine.h/.cpp` | VM component owning SoC and driver |
|
|
|
|
## Future Work
|
|
|
|
- **Windows**: Named pipe transport, KMD escape driver backend
|
|
- **RDMA**: Rack-scale transport for distributed simulation
|
|
- **Platform-independent commands**: Abstract command enum for cross-platform
|