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

304 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Race Detector
Detects synchronization hazards in AMD GPU kernels. Tracks in-flight memory
events (loads, stores) and reports races when a register or LDS memory is
accessed before the operation that produced it has been properly synchronized.
Currently detects intra-workgroup races — cases where the value read from a
register or LDS is not deterministic due to missing `s_waitcnt` or `s_barrier`
instructions.
## Quick start
This section is a standalone guide to getting up and running with race
detection. For general rocjitsu build and usage instructions, see the
[README](../README.md). For an overview of the plugin system and sink
configuration, see [plugins.md](plugins.md).
Build rocjitsu (see [building.md](building.md) for details). You don't need a
physical GPU — the emulator runs entirely on the CPU.
```bash
cd emulation/rocjitsu
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
```
### Example: detecting a missing barrier
Here's a small HIP kernel with a missing `__syncthreads()`. Each thread writes
to shared memory (LDS) and then reads from where another thread wrote — without
a barrier. The reads race with the writes.
```c++
// race_example.hip
#include <hip/hip_runtime.h>
#include <cstdio>
__global__ void transpose_lds(const int *in, int *out) {
__shared__ int tile[128];
int tid = threadIdx.x;
tile[tid] = in[tid];
// BUG: missing __syncthreads() — the read below may see
// another thread's write before it has completed.
out[tid] = tile[127 - tid];
}
int main() {
int *d_in, *d_out;
hipMalloc(&d_in, 128 * sizeof(int));
hipMalloc(&d_out, 128 * sizeof(int));
transpose_lds<<<1, 128>>>(d_in, d_out);
hipDeviceSynchronize();
hipFree(d_in);
hipFree(d_out);
printf("done\n");
}
```
Compile it with `hipcc` or `amdclang++`. The `--offload-arch` must match the
emulated GPU, which depends on the config file you pass to rocjitsu (e.g.
`gfx950_cdna4.json` emulates gfx950). If using `amdclang++`, pass `-O1` or
higher — the emulator does not currently support unoptimized (`-O0`) GPU code
objects. `hipcc` defaults to `-O3` so this isn't an issue there.
```bash
hipcc -o /tmp/race_example race_example.hip --offload-arch=gfx950
# or: amdclang++ -O2 -o /tmp/race_example race_example.hip --offload-arch=gfx950
```
Enable the race detector by adding it to the `plugins` section of your
rocjitsu config file (`my_config.json`):
```json
{ "plugins": { "race": {} } }
```
Run it under the emulator:
```bash
$BUILD_DIR/tools/rocjitsu/rocjitsu --config my_config.json -- /tmp/race_example
```
You should see output:
```
RACE kernel=transpose_lds symbol=_Z13transpose_ldsPKiPi dispatch=1 type=LDS reg=508 wave=0 lane=0 wg=0,0,0 conflict=unknown
Race on LDS byte 508 [workgroup (0, 0, 0), wave 0, lane 0]
==> ds_write_b32 v0, v1 ; <-- wave 1
v_sub_u32_e32 v1, 0, v0
==> ds_read_b32 v1, v1 ; <-- wave 0 lane 0
END_RACE
```
This tells you that dispatch 1 of `transpose_lds` reported a race: wave 1 wrote
to LDS (`ds_write_b32`) and wave 0 read from the same address (`ds_read_b32`)
without a barrier in between. The `kernel` field is a compact display name, and
`symbol` is the exact ELF symbol when rocjitsu can resolve it. If a name cannot
be resolved, rocjitsu reports `?` for the unresolved field. The fix is to add
`__syncthreads()` between the write and the read.
### Running your own application
Replace the binary path with your application. This works with any ROCm workload
— compiled HIP/HSA binaries, Python scripts using PyTorch or JAX, multi-process
launchers like `torchrun`, etc.
```bash
$BUILD_DIR/tools/rocjitsu/rocjitsu --config my_config.json -- ./my_app
$BUILD_DIR/tools/rocjitsu/rocjitsu --config my_config.json -- python my_script.py
```
To capture reports to a file instead of stderr (useful for CI or
scripted workflows), add a `sinks` section to your config:
```json
{
"plugins": { "race": {} },
"sinks": { "types": ["file"], "dir": "/tmp/output" }
}
```
```bash
$BUILD_DIR/tools/rocjitsu/rocjitsu --config my_config.json -- ./my_app
# Reports are written to /tmp/output/race.log
```
## What is a race?
A race occurs when the value read from a register or LDS memory is 'ambiguous'
due to unsynchronized access. By 'ambiguous' we mean that it is theoretically
possible to observe different values on different runs. On AMD GPUs, correct use
of `s_waitcnt` (to wait for a wave's own memory operations to complete) and
`s_barrier` (to synchronize waves within a workgroup) is required to avoid
races. Some examples:
1. A wave issues a global load into a VGPR, then reads that VGPR before issuing
`s_waitcnt vmcnt(0)`. The load may not have completed, so the read value is
undefined.
1. One wave in a workgroup writes to an LDS address. Another wave reads from
the same address without an intervening `s_barrier`. The read may see stale
data because the write may not have completed from the reader's perspective.
## What this plugin detects
- **VGPR races**: a vector register is read before a pending global or LDS load
has completed (`s_waitcnt vmcnt` / `s_waitcnt lgkmcnt` insufficient).
- **SGPR races**: a scalar register is read before a pending scalar load has
completed (`s_waitcnt lgkmcnt` insufficient).
- **LDS races**: an LDS byte is read or written by one wave while another wave
has an outstanding write to the same byte, without an intervening `s_barrier`.
Detection is at byte granularity: D16 (half-register) loads only flag races on
the affected bytes, and LDS races are tracked per byte.
## How it works
Every in-flight memory operation has an **event** that goes through the
following lifecycle:
1. **ACTIVE** — the operation is in flight.
1. **WAVE_COMPLETE** — `s_waitcnt` has retired the event for the owning wave.
This means the event is no longer in flight from the perspective of the wave
that issued the operation, but is still in flight from the perspective of
other waves in the same workgroup.
1. **RETIRED** — `s_barrier` has synchronized all waves. The event is fully
retired and, from the perspective of all threads in all wavefronts, the
operation is complete.
The plugin keeps track, for all registers and LDS memory bytes, of which memory
operations are in flight. When an instruction in the emulator accesses an LDS
byte, there is a check to see what memory events are still in flight that
read/write that byte, from the perspective of the accessing thread. In this way,
RAW (read-after-write) and WAR (write-after-read) hazards can be detected.
Similar logic applies for VGPR and SGPR accesses.
**LDS race detection** uses coarse-grained counters (one per 16-byte chunk) for
fast-path checks, with interval-based overlap scanning as a fallback. Live
events are split by direction so that RAW and WAR hazards are checked
independently.
**VGPR race detection** tracks events per register, using the stored exec mask
to determine which lanes are affected. Tracking is at byte granularity within
each 32-bit VGPR so that D16 instructions do not cause false positives when the
other half is accessed independently.
### Worked example
The transpose kernel above compiles to something like this (simplified):
```asm
ds_write_b32 ... ; tile[tid] = in[tid]
s_waitcnt lgkmcnt(0) ; wait for own LDS write
; BUG: no s_barrier here
ds_read_b32 ... ; out[tid] = tile[127 - tid]
```
Two waves in the same workgroup execute this code. Each wave's write address
overlaps the other wave's read address (because `127 - tid` crosses the wave
boundary). Below is one possible interleaving — the detector will find the race
regardless of the order the waves execute in:
1. **Wave 0 executes `ds_write_b32`.** The detector allocates an event for the
LDS write. Status: **ACTIVE**. The event records the byte range written
(lane 0 writes bytes 0–3 for `tile[0]`) and is added to the live write list.
Coarse-grained write counts (one counter per 16-byte chunk) for the affected
region are incremented.
1. **Wave 0 executes `s_waitcnt lgkmcnt(0)`.** This drains wave 0's lgkmcnt
counter. The write event transitions from **ACTIVE** to **WAVE_COMPLETE**. It
is now safe for wave 0 to access those bytes, but the event remains in the
live write list — other waves have not synchronized yet.
1. **Wave 1 executes `ds_write_b32`.** A separate event is allocated for wave
1's write (lane 63 writes bytes 508–511 for `tile[127]`). Status: **ACTIVE**.
1. **Wave 1 executes `s_waitcnt lgkmcnt(0)`.** Wave 1's event transitions to
**WAVE_COMPLETE**.
1. **Wave 0 executes `ds_read_b32`.** Lane 0 reads `tile[127 - 0]` = bytes
508–511, the address wave 1 wrote. The detector validates that no live writes
overlap:
- *Fast path*: the write count for the 16-byte chunk containing byte 508 is
non-zero (wave 1's write is still live). Falls through to slow path.
- *Slow path*: scans live write events. Finds wave 1's event covering bytes
508–511. The event is **WAVE_COMPLETE**, not **RETIRED**, and the accessing
wave (0) differs from the owning wave (1).
- **Race reported.**
1. **What `s_barrier` would fix.** If an `s_barrier` had appeared between steps
4 and 5, the detector would flush all **WAVE_COMPLETE** events to
**RETIRED**: they are removed from the live write list and the chunk-level
write counts are decremented back to zero. The subsequent read validation
finds no live writes — no race.
## Directory layout
Source files are under `lib/rocjitsu/src/rocjitsu/vm/plugins/race_detector/`:
```
race_detector/
├── plugin.h/.cpp rocjitsu plugin adapter (translates hooks to core API)
└── core/ detection algorithm (does not depend on rocjitsu types)
├── race_detector.h/.cpp main detector: event allocation, validation, retirement
├── wave_race_state.h/.cpp per-wave state: register tracking, waitcnt resolution
├── event_registry.h append-only event store with prefix trimming
├── interval_set.h half-open byte range tracking for LDS
├── types.h core enum types and structs
├── common_register.h SGPR/VGPR classification helpers
├── dim3d.h 3D coordinate helpers
└── profiler_interface.h optional hook profiling
```
## Tests
Tests are part of the rocjitsu test suite (`emulation/rocjitsu/tests/`):
- `race_detector_tests.cpp` — drives `RaceDetector` and `WaveRaceState` directly
via `race_test_builder.h`, covering VGPR, SGPR, LDS, D16, DTL, exec mask,
multi-workgroup, and mixed counter scenarios.
- `interval_set_tests.cpp` — unit tests for `IntervalSet`.
- `hip_race_gfx950_test.hip` and `hip_race_gfx1151_test.hip` — end-to-end HIP
kernel tests run under the emulator with the `race` plugin enabled in the
config file.
```bash
# Core detection tests
ctest --test-dir build -R "RaceDetector|IntervalSet"
# End-to-end HIP tests (the test config enables the race plugin)
ctest --test-dir $BUILD_DIR -R "RaceTest"
```
## Limitations
- **Performance**: rocjitsu emulates GPU execution on the CPU, which is orders
of magnitude slower than running on actual hardware. Race detection adds
further overhead on top of emulation. This is a correctness tool, not a
performance tool — use small inputs and targeted test cases rather than full
production workloads.
- **Intra-workgroup only**: the detector tracks races within a single workgroup
(missing `s_waitcnt` and `s_barrier`). It does not detect inter-workgroup
races, races between dispatches, or host-device synchronization issues.
- **No WAW detection**: write-after-write hazards are not currently flagged.
This includes both LDS WAW (two waves writing to the same LDS byte without a
barrier) and VGPR WAW (an ALU instruction overwriting a register that has a
pending global load).
- **Kernel name resolution**: kernel names in race reports may show as `"?"` if
symbol information is not available in the code object.
## History
The race detection logic was originally developed as part of **race-emulator**,
a standalone CPU-side GPU assembly emulator that parsed `.s` assembly text
files.
The detection logic is independent of any particular emulation approach — it
operates on abstract memory events (register loads, LDS accesses, waitcnt,
barrier) regardless of how those events are produced. The emulation part of
race-emulator is no longer under development; rocjitsu is used for emulation.