Skip to content

feat(emulation): debug emulated GPU kernels with real ROCgdb - #9844

Merged
atgutier merged 112 commits into
developfrom
users/arosa/rj/rocgdb/gold
Aug 19, 2026
Merged

atgutier merged 112 commits into
developfrom
users/arosa/rj/rocgdb/gold

Conversation

@amd-arosa

@amd-arosa amd-arosa commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Debugging a GPU kernel has required a physical AMD GPU. rocjitsu already emulates
the AMD KFD closely enough to run real ROCr/HIP workloads, but it had no debug
surface: AMDKFD_IOC_DBG_TRAP was unimplemented, waves could not be stopped, and
there was no CWSR state for a debugger to read or write.

This PR closes that gap. Real ROCgdb and real librocm-dbgapi, unmodified, now
attach to a workload running on the emulated GPU and debug its kernels end to end
,
with no AMD hardware present: breakpoints, single-step, watchpoints, multi-wave
info threads, source-level locals out of private/scratch, SIGILL and SIGSEGV on GPU
faults, and clean detach. One command drives it:

mirage run --profile mi350x --gdb -- ./my_hip_app

The bar this is measured against is the official ROCgdb gdb.rocm test suite, which
is written for hardware and knows nothing about the emulator. All 89 of its files
pass.

Technical Details

All KFD debug behaviour is mirrored from the real driver source
(amd/amdkfd/{kfd_chardev.c,kfd_debug.c,kfd_topology.c}, amdgpu-6.16.13),
cross-checked against a physical MI300X and against the in-tree
projects/rocdbgapi client. Changes are confined to emulation/.

KFD debug protocol (kmd/linux/{simulated_kfd,remote_driver,rj_daemon,interposer})

  • DBG_TRAP sub-operation dispatcher with the driver's validation ladder, authorized
    by a real ptrace relationship and marshalled over the daemon. The debugger and the
    inferior are separate processes, so in-process mode cannot host this and daemon mode
    is mandatory.
  • Debug sessions keyed by inferior pid, so ROCgdb can attach before the inferior ever
    opens /dev/kfd; full attach/detach lifecycle, GET_DEVICE_SNAPSHOT,
    GET_QUEUE_SNAPSHOT, SET_EXCEPTIONS_ENABLED / SET_FLAGS, and
    SET_WAVE_LAUNCH_OVERRIDE validated against the gfx9.4 trap mask.
  • Debuggee memory access authorized through the daemon (debugger-opened proc memory
    and pinned process descriptors travel with the enable request), preserving remote
    DONTFORK semantics and naming the synthetic KFD marker as /dev/kfd in proc-map
    snapshots so fork and gcore behave as they do on hardware.
  • Runtime enable/disable handshake reported only to an attached debugger, -ESRCH
    (not -EPERM) once the target's KFD state is gone, and suspend/resume of
    already-destroyed queues answered so rocdbgapi can retire them during teardown.

Engine wave-stop and CWSR (vm/amdgpu/{compute_unit,wavefront,command_processor},
kmd/linux/cwsr.{h,cpp})

  • Waves stop on s_trap for the KFD debugger; stopped-wave state is serialized into
    the real CWSR area layout and written back on resume.
  • The runtime's own gfx9.4 trap handler is executed rather than shimmed. A wave frozen
    inside it needs the interrupted architectural state published, not the handler's,
    so EXEC and STATUS are shadowed in both directions: a debugger's edit lands on the
    state s_rfe restores instead of the register the handler is still running under.
  • Deterministic single-step via TRAPSTS.trap_after_inst; reserved debugger memory for
    displaced stepping; GPU address watchpoints; illegal instruction reported as SIGILL;
    memory violation reported as SIGSEGV; private/scratch reads so print and
    info args resolve.
  • Multi-wave: atomic queue resume, wave reclaim on queue destroy, scheduler-locked
    residency, 3D workgroup coordinates in CWSR records, and gfx950 LDS preservation.
  • Bounded functional-mode scheduling quantum while debugging so active CUs make fair
    progress without starving peer waves.

ISA and memory fixes surfaced by the suite (isa/arch/amdgpu/, vm/amdgpu/)

Running real debugger workloads exercised paths nothing else had: linked helper calls
resolved against the GETPC base, ALU exception classification in generated execute
paths, scalar memory access faults, signed scratch offsets, unaligned swizzled scratch,
cross-XCD atomic coherence, the v_bcnt accumulator, pageable host copies through SDMA,
and s_sleep's architected delay. Generator changes live in lib/python/amdisa/ with
the generated output regenerated, not hand-edited.

Debugger UX, docs, demos. mirage run --gdb / --gdb-ex;
docs/rocgdb-debugging.md; four recorded walkthroughs under demos/ (quickstart,
watchpoint, fault, multi-wave), each with a .md explainer, a runnable .sh, and an
asciinema .cast.

Test tooling. tools/run_rocgdb_official.py runs each gdb.rocm file in a fresh,
bounded, verified Mirage daemon session and records a per-test summary plus backend
hashes; tools/repeat_rocgdb_test.sh repeats a subset serially under an flock (two
concurrent Mirage sessions collide and manufacture failures) for chasing intermittents.
Both archive the daemon's host.log alongside gdb.log. The emulated KFD's
diagnostics land there and nowhere else, and three debugger defects fixed in this PR
were invisible until it was collected.

Issue Tracking

Epic: #8276

Closes #8364, closes #8365, closes #8366, closes #8367, closes #8368, closes #8369,
closes #8370, closes #8371, closes #8372, closes #8373, closes #8374, closes #8375,
closes #8376, closes #8377, closes #8378, closes #8379, closes #8380, closes #8381.

Item Work
#8364 Authorize DBG_TRAP by real ptrace + daemon transport
#8365 Key debug sessions by inferior pid so rocgdb can attach
#8366 Complete the DBG_TRAP attach/detach lifecycle
#8367 Enumerate compute queues in GET_QUEUE_SNAPSHOT
#8368 Stop a GPU wave on s_trap for the KFD debugger
#8369 Serialize stopped-wave state into the CWSR area layout
#8370 Raise a debug event and serialize the CWSR when a wave traps
#8371 Drive GPU wave debugging end to end with ROCgdb
#8372 mirage run --gdb one-command debugging
#8373 Deterministic single-step via TRAPSTS.trap_after_inst
#8374 Reserve debugger memory for step-over
#8375 Validate SET_WAVE_LAUNCH_OVERRIDE against the gfx9.4 trap mask
#8376 GPU address watchpoints
#8377 Illegal instruction reported as SIGILL
#8378 Memory-access violation reported as SIGSEGV
#8379 Private/scratch reads for print / info args
#8380 Multi-wave GPU kernel debugging
#8381 ROCgdb debugging guide and demos index

Built on the already-merged foundations #8280, #8281, #8353 and #8363.

Test Plan

  1. Official ROCgdb suite. All 89 gdb.rocm test files, each in its own fresh,
    bounded Mirage daemon session, run serially via
    tools/run_rocgdb_official.py --profile mi350x. The suite is upstream ROCgdb's own,
    written for hardware and unmodified.
  2. rocjitsu unit/integration suite. Full non-benchmark ctest run, including the 66
    test cases added here (19 in the new wave_debug_test.cpp, 34 in kfd_ioctl_test.cpp,
    the rest across shared-infra, VM, interposer and decode tests).
  3. End-to-end smoke test in CI. New RocgdbDebug.KernelBreakpoint ctest target drives
    real ROCgdb through mirage run to set a kernel breakpoint, stop, read wave registers,
    and continue to a correct result. Gated on rocgdb and hipcc being present; skips
    cleanly otherwise.

Test Result

Both suites green at c998c5cd (branch tip):

Suite Result
Official ROCgdb gdb.rocm 89/89 files pass, 0 FAIL. 2376 PASS over 2407 executed assertions (9 UNSUPPORTED, 7 KFAIL, 3 XFAIL, 6 WARNING, 6 UNTESTED)
rocjitsu ctest (non-benchmark) 2492 tests, 0 failures (4 skipped by design)

Submission Checklist

amd-arosa and others added 30 commits August 4, 2026 21:38
… the daemon

## Motivation

ROCgdb must issue DBG_TRAP ioctls against a different client process (the inferior). The landed dispatcher still rejected the first cross-process ENABLE because it compared against a debugger pid recorded only after ENABLE succeeds.

## Technical Details

- Authorize non-self DBG_TRAP operations from the inferiors live `/proc/<pid>/status` `TracerPid`, matching the kernels ptrace-parent gate.
- Implement SET_EXCEPTIONS_ENABLED by updating the debug sessions forwarded-exception mask.
- Keep develops reworked daemon transport unchanged: DBG_TRAP remains marshaled by the landed RPC path, and ENABLE transfers the notifier with SCM_RIGHTS rather than the prototypes raw-fd approach.
- Add a real ptrace test that rejects an unattached caller, authorizes it after PTRACE_ATTACH, uses a valid notifier fd, exercises SET_EXCEPTIONS_ENABLED, and cleans up the child deterministically.

Prototype of #8364.

## Test Plan

- Build rocjitsu and mirage.
- Run the KFD, sysfs, and daemon suites.
- Run the mirage workspace tests.

## Test Result

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='KfdIoctlTest.*:DbgTrapPtraceTest.*:RemoteDriverDbg*'`: 31/31 passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
ROCgdb enables debugging on an inferior immediately after exec, before ROCr opens /dev/kfd. SimulatedKfd previously required a KfdProcess for the target pid and returned ESRCH.

- Store DebugSession objects in SimulatedKfd keyed by inferior Linux pid, independent of KfdProcess and /dev/kfd connection lifetime.
- Pin target and debugger identities with pidfds, revalidate them around authorization and session creation, and reject PID-reuse/exit races with ESRCH.
- Preserve live debug sessions across target or debugger KFD connection close, while asynchronously disabling sessions and releasing notifier ownership when either process exits.
- Match Linux target-liveness ordering by releasing an exited target's stale session while returning ESRCH from DISABLE.
- Make reaper shutdown race-free with C++20 stop-token-aware condition_variable_any waits.
- Retain the landed DBG_TRAP dispatcher, runtime-info marshalling, RPC path, and SCM_RIGHTS notifier ownership.
- Add deterministic coverage for exited-target DISABLE cleanup, debugger-exit cleanup and re-enable, identity races, KFD-close retention, and reaper shutdown.

Prototype of #8365.

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='KfdIoctlTest.*:DbgTrap*'`: 41/41 passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
Replayed the attach/detach configuration operations onto the landed SimulatedKfd DBG_TRAP dispatcher and SCM_RIGHTS/RPC transport. SET_FLAGS returns prior state; launch mode and override validate their enums and report the prior/supported masks expected by rocm-dbgapi; idle event queries return EAGAIN; queue snapshots remain empty; queue suspend/resume report zero operations; and address-watch placeholders retain the existing runtime and GPU validation gates. DISABLE continues to use HEAD’s hardened pidfd lifecycle: an exited target releases the session/notifier and returns ESRCH rather than bypassing target liveness. Fresh sessions reset all configuration state.

Review fixes made the no-queue suspend/resume result truthful for non-empty requests, reported the full synthetic trap-support mask used by rocm-dbgapi probes, rejected invalid launch values, preserved the landed pid-identity/debugger-exit cleanup, and made the documentation explicit that GPU breakpoints and wave debugging remain pending. Deterministic tests cover success, validation errors, idle behavior, non-empty no-queue requests, state reset, and exited-target cleanup.

Prototype of #8366.

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='KfdIoctlTest.*:DbgTrap*'`: 42/42 passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `mirage run --gdb` is introduced by stack row 12 and was not available at this step.
rocm-dbgapi locates a process's waves by walking each compute queue's
context-save-restore area, discovered through GET_QUEUE_SNAPSHOT. Report the
live queue geometry instead of an empty snapshot so wave discovery can build
on the existing hardened debug-session lifecycle.

Prototype of #8367.

- Publish queue metadata after CP registration, update it with UPDATE_QUEUE,
  and remove it atomically on destroy/close under the allocation lock.
- Match the amdkfd two-call ABI, creation order, input stride, entry-size
  clamping, total counts, EC_QUEUE_NEW reporting/clearing, and errno behavior.
- Marshal queue snapshots through daemon RPC with checked inline lengths,
  pointer reconstruction, success-only strided copyback, and malformed-request
  rejection.
- Preserve null queue and device snapshot pointers through daemon
  reconstruction, return EFAULT, and prevent null client copyback.
- Add deterministic local, daemon-dispatch, transport failure, null-pointer,
  capacity, padding, lifecycle, and metadata tests.

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='KfdIoctlTest.*:RemoteDriverDbg*'`: 45/45 passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `mirage run --gdb` is introduced by stack row 12 and was not available at this step.
Prototype of #8368.

Adapt the prototype to the landed dispatch and scheduler architecture. Preserve global-workgroup EXEC masking while tagging both CP and SPI dispatch paths with the KFD queue ID. Decode s_trap through the generated ISA mnemonic, preserve STATUS/TRAPSTS, save the post-trap PC, and quiesce debug-halted waves without retiring their slots or spinning CU/SPI schedulers. Keep CWSR serialization, event delivery, and asynchronous resume wiring for their later stack commits.

Use runnable-wave idle semantics when the CP records CU state and schedules execution. This reactivates a quiesced CU when another CU's idle callback dispatches work beside a resident debug-halted wave, while avoiding activation when only stopped waves remain. Add a deterministic two-CU regression that halts one wave, drains its event loop, unblocks a new dispatch from another CU, and proves the new wave progresses; the regression fails against the prior active-wave check.

Add deterministic CDNA4 and gfx12.5 tests for accepted, absent, and declined handlers, immediate decoding, PC/status preservation, stop/quiesce/resume state, queue identity, reset cleanup, and post-quiescence dispatch reactivation.

Results:
- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='WaveDebugTest.*:CommandProcessorTest.DispatchToQuiescedDebugHaltedCuReactivatesEventLoop:IsaTest.*Trap*'`: 6/6 passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `mirage run --gdb` is introduced by stack row 12 and was not available at this step.
Prototype of #8369.

## Motivation

rocm-dbgapi reads stopped-wave registers from the inferior-visible queue
context-save-restore area. Serialize the daemon-hosted emulator state using
the exact gfx9.4 CWSR control-stack and wave-save layout it parses.

## Technical Details

- Write the CWSR header, control stack, and wave64 HWREG/TTMP/SGPR/VGPR
  blocks at rocdbgapi-compatible offsets.
- Encode gfx942/gfx950 packet, wave, workgroup, trap, and register metadata;
  explicitly exclude earlier gfx9 layouts whose ACC-VGPR and TTMP packet-id
  conventions differ.
- Validate alignment, field widths, register limits, area bounds, and address
  overflow before writing; zero padding and publish payload before the header.
- Preserve workgroup boundaries and document queue suspension, coherent mapping,
  locking, and wave-lifetime requirements for callers.
- Add a deterministic rocdbgapi-formula round-trip covering multiple waves and
  workgroups plus invalid-input no-write cases.

## Test Results

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='WaveDebugTest.Cwsr*'`: passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- Generator not run: no generated source changed.
- `mirage run --gdb` is introduced by stack row 12 and was not available at this step.
…e traps

With the CWSR serializer (prior PR) and the wave trap state in place, the driver
can turn a GPU-kernel breakpoint into something rocm-dbgapi observes: when a wave
executes `s_trap`, the emulated KFD must stop it, publish its state into the
queue's CWSR area, and report an `EC_QUEUE_WAVE_TRAP` event through
QUERY_DEBUG_EVENT — the sequence rocm-dbgapi drives on every wave stop. This
wires those unit-tested pieces into the live driver path.

rocm-dbgapi polls QUERY_DEBUG_EVENT for a queue with a pending exception, then
reads that queue's CWSR area. A queue must also have been announced with
`EC_QUEUE_NEW` before dbgapi accepts wave events on it.

- Install `SimulatedKfd::on_wave_trap` as each compute unit's `s_trap` handler
  at CP-init. Running on the engine thread when a wave traps, it resolves the
  wave's process and its queue's CWSR area (returns false — s_trap is a no-op —
  if undebugged or no CWSR area), assigns the wave a stable debugger id, marks it
  debug-halted, serializes all debug-halted waves of the queue via
  `serialize_queue_cwsr`, raises `EC_QUEUE_WAVE_TRAP`, and wakes the debugger's
  notifier pipe.
- Hold debug events per target, grouped by queue, in `debug_events_` (a separate
  lock from `debug_sessions_`, never held together on the trap path);
  `raise_debug_event` also reflects the exception onto the queue snapshot.
- QUERY_DEBUG_EVENT drains real events: return one queue whose pending exception
  intersects the requested mask, set queue_id/gpu_id, clear the requested bits,
  and return -EAGAIN when none remain (matching `kfd_dbg_ev_query_debug_event`).
- `Wavefront` gains a stable `debug_wave_id` (planted in TTMP4:5);
  `build_cwsr_wave_state` snapshots pc/exec/vcc/status(+HALT)/mode/m0/SGPRs/VGPRs
  and the debugger TTMP metadata for the serializer.

- This is the driver wiring that connects the already-unit-tested wave-stop and
  CWSR-serialization pieces; the RUNTIME_ENABLE handshake and register write-back
  that complete a full ROCgdb breakpoint, plus the end-to-end CI harness that
  exercises this path with real ROCgdb, land in the next PR.

Prototype of #8370.

- Re-run the full gtest suite: the wave-trap and CWSR round-trip unit tests cover
  the pieces this wires together, and the QUERY_DEBUG_EVENT drain is on the
  attach/detach path already covered.

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='KfdIoctlTest.DbgTrapRealWaveTrapPublishesCwsrAndEvent:WaveDebugTest.*:KfdIoctlTest.DbgTrap*'`: 32/32 passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `mirage run --gdb` is introduced by stack row 12 and was not available at this step.
The prior PRs stop a wave, serialize it into the CWSR area, and raise the event.
This closes the loop so real ROCgdb, through `mirage run`, sets a breakpoint on a
GPU kernel, stops the trapping wave, reads its registers/PC/disassembly, and
continues to a correct result — the first fully working GPU-kernel debug session
on the emulator, with a CI harness that proves it.

Two more pieces complete a breakpoint: ROCr's `RUNTIME_ENABLE` blocks under a
debugger until it receives `SEND_RUNTIME_EVENT` (this is what makes a pending
`break <kernel>` resolve as the code object loads), and on resume the debugger's
register edits — written straight into the CWSR area — must be read back and
applied to the live wave.

- Implement the RUNTIME_ENABLE debugger handshake: raise `EC_PROCESS_RUNTIME`,
  wait (bounded) for the debugger's ack, and return the saved `kfd_runtime_info`
  from QUERY_EXCEPTION_INFO so dbgapi installs `r_brk` and tracks code objects.
  Pass the debugger's `dbg_fd` notifier pipe to the daemon out-of-band
  (SCM_RIGHTS on ENABLE) so a wave stop can wake it (mirage `daemon.rs`,
  `rocjitsu_sys`).
- Add `deserialize_queue_cwsr` and `apply_cwsr_to_wave`: RESUME_QUEUES reloads a
  wave from its CWSR area (picking up the debugger's edits) and unhalts it.
- Marshal QUERY_EXCEPTION_INFO over the daemon transport.
- Add the `RocgdbDebug` CTest harness (`tests/rocgdb/debug_kernel.sh` +
  `add_one.hip`): it compiles a HIP kernel with device debug info and drives real
  ROCgdb through `mirage run` — break on the GPU kernel, stop at the wave, read
  PC/EXEC and the instruction at PC, and continue to a correct result —
  asserting each marker so it is a CI smoke test for the whole stack. It skips
  cleanly when hipcc/rocgdb/mirage are unavailable.

- Deterministic single-step and stepping over a breakpoint (displaced stepping)
  are the next two PRs; here `continue` past the entry breakpoint already works.

Prototype of #8371.

- Add `CwsrDeserializeRecoversSerializedWaveState` (serialize -> deserialize
  round-trips the register state the resume path reads back).
- Run the full gtest suite, and the `RocgdbDebug` CTest end to end.

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='WaveDebugTest.*:KfdIoctlTest.DbgTrap*:RemoteDriverDbg*'`: 43/43 passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `ctest --test-dir build --output-on-failure -R '^RocgdbDebug\.'`: no tests registered because the venv ROCgdb was not yet discoverable on `PATH` at this step; final-stack real ROCgdb coverage is recorded in row 21.
Add `mirage run --gdb`, a one-flag shortcut that launches the workload
under ROCgdb for GPU kernel debugging on the emulated GPU.

Debugging an emulated GPU kernel meant hand-writing the full ROCgdb
invocation (`mirage run -- rocgdb -ex 'set breakpoint pending on'
--args ./app`) and knowing that kernel breakpoints must be pending
because GPU code objects load lazily at dispatch. `mirage run --gdb`
collapses that into one flag, making the emulated GPU debugger the
first-class, discoverable way to debug a kernel with no AMD GPU.

`mirage run` already starts a session daemon and launches the workload
under the rocjitsu interposer; ROCgdb only needs to be spliced in front
of the workload argv. Local (in-process) mode cannot host ROCgdb, so the
daemon path (the default) is required.
- Add `--gdb` and repeatable `--gdb-ex <CMD>` flags to `mirage run`;
  `--gdb-ex` implies `--gdb`.
- `gdb_wrap_argv` rewrites `./app args...` to
  `rocgdb [--batch] -ex 'set breakpoint pending on' [-ex CMD]... --args
  ./app args...`: pending breakpoints so `break <kernel>` resolves at
  dispatch, `--gdb-ex` commands injected in order for scripted runs, and
  `--batch` added only when `-ex` commands are present so a bare `--gdb`
  stays interactive.
- Add `scripts/record_demo.sh` (asciinema recorder) and the
  `rocgdb-quickstart` demo (`.md`/`.sh`/`.cast`).
- Document the one-command form in `rocgdb-debugging.md`.
Requires a profile whose container ships ROCgdb, and `hipcc`/`rocgdb` on
PATH to build and debug. Reading kernel arguments (`print n`) needs
private/scratch reads, which land in a later change; the quickstart demo
inspects the wave PC and disassembly for now.

Prototype of #8372.

- Unit-test `gdb_wrap_argv`: bare wrap makes breakpoints pending; extra
  `--gdb-ex` commands are injected in order and enable `--batch`.
- Drive the quickstart demo end to end against `add_one.hip`.

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='WaveDebugTest.*:KfdIoctlTest.DbgTrap*'`: passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `emulation/mirage/target/debug/mirage run --profile mi350x --gdb -- /bin/true`: CLI path executed and failed accurately with `command not found: rocgdb`; ROCgdb is not installed.

`emulation/rocjitsu/demos/rocgdb-quickstart.{md,sh,cast}`: debug a real GPU kernel with `mirage run --gdb` -- no AMD GPU required.
…trap_after_inst

Raise TRAPSTS.TRAP_AFTER_INST on single-step completion so rocm-dbgapi
decodes the stop as a single-step instead of re-running the wave.

Back-to-back `stepi` in ROCgdb was deterministically broken against the
emulator: one `stepi` worked, but a second ran the wave away -- the engine
executed the rest of the kernel and the debugger lost control. This blocked
instruction single-stepping, the last gap in the core
breakpoint/step/continue debugging loop.

On gfx9.4 (MI300/MI350), single-step *completion* is signaled to rocm-dbgapi
by TRAPSTS.TRAP_AFTER_INST (bit 25), not by the wave merely being stopped.
dbgapi's `wave_get_state` maps that exact bit to
`WAVE_STOP_REASON_SINGLE_STEP` (rocdbgapi architecture.cpp:1544, mask
`sq_wave_trapsts_trap_after_inst_mask = 1u << 25` at :3782); it is the same
bit dbgapi sets itself in `simulate_instruction_fixup` (:4044).
- `SimulatedKfd::on_wave_single_step_complete` now sets TRAPSTS.TRAP_AFTER_INST (named
  constant `kSqWaveTrapstsTrapAfterInstMask`, cited to the dbgapi register
  definition) before serializing the post-step CWSR, so the wave the debugger
  reads back carries the single-step stop reason.
- The bit never accumulates: dbgapi clears it on the next resume via
  `clear_stop_reasons` (:1418), and the breakpoint path (non-zero trap id)
  does not set it, so breakpoints still report as breakpoints.
- `debug_kernel.sh` now single-steps three instructions and asserts the four
  sampled `$pc` values (breakpoint + each step) are strictly increasing -- a
  runaway would exit early or repeat a PC.
Scoped to gfx9.4 single-step semantics; other stop reasons (watchpoints,
faults) are added by later changes.

Prototype of #8373.

- `ninja librocjitsu.so rocjitsu_tests`; full unit suite.
- `ctest -R RocgdbDebug` (break, run, 3x stepi with PC assertions, continue)
  against the freshly built librocjitsu.so.

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='WaveDebugTest.SingleStepExecutesOneInstructionThenReports:WaveDebugTest.Cwsr*'`: 3/3 passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `emulation/mirage/target/debug/mirage run --profile mi350x --gdb -- /bin/true`: returned `command not found: rocgdb` because the venv ROCgdb was not yet discoverable on `PATH` at this step; final-stack real ROCgdb coverage is recorded in row 21.
…l breakpoints

Advertise a per-queue debugger displaced-stepping region in the CWSR
header so rocm-dbgapi can step over an interior breakpoint.

Setting a breakpoint on an interior kernel line and issuing `continue` (or
single-stepping an instruction rocm-dbgapi cannot simulate) aborted the
debug session with "Per-queue memory reserved for the debugger is missing
/ amd_dbgapi_displaced_stepping_start failed". Only breakpoints on
instructions dbgapi can emulate in software could be stepped over, so
`continue` from a real interior breakpoint was unusable. Stepping over a
breakpoint is a fundamental debugger operation.

To resume past a breakpoint, rocm-dbgapi uses displaced stepping: it copies
the instruction under the breakpoint into a per-queue scratch buffer,
single-steps it there, then fixes up the PC. Those buffers are carved in
32-byte chunks from a "debugger memory" region the context-save header
advertises via `debug_offset` (byte 16) and `debug_size` (byte 20); dbgapi
aborts if it is absent (rocdbgapi queue.cpp:111,
`allocate_displaced_instruction`).
- `serialize_queue_cwsr` reserved `debug_offset = debug_size = 0`, so the
  region was missing. It now reserves the region in the tail slack of the
  save area (64-byte aligned, so each 32-byte chunk is aligned) and writes
  the header fields. The region lives inside the inferior-mapped
  context-save area, so it is backed by the same memory dbgapi reads and
  the emulator fetches the displaced instruction from directly.
- Size follows the real KFD formula (amdgpu kfd_queue.c:398-460:
  `ALIGN(wave_num * DEBUGGER_BYTES_PER_WAVE, DEBUGGER_BYTES_ALIGN)`,
  `PER_WAVE = 32`, `ALIGN = 64`) plus headroom chunks for dbgapi's per-queue
  park and terminating instruction buffers. Constants are named and cited.
- The region is skipped (fields stay 0) if it would not fit the advertised
  save-area size. `CwsrLayout` now reports the chosen `debug_offset`/`debug_size`.
Sizing targets the gfx9 CDNA save-area layout; the headroom is fixed rather
than negotiated per debug session.

Prototype of #8374.

- Unit: new `WaveDebugTest.CwsrReservesDebuggerMemoryForDisplacedStepping`
  asserts a non-zero, 64-aligned, in-bounds, non-overlapping region with
  enough chunks; CWSR round-trip/deserialize tests still pass. Full suite.
- E2E: `debug_kernel.sh` (CTest `RocgdbDebug`) gains an interior-breakpoint
  (`add_one.hip:15`) + `continue` scenario, asserting the kernel completes
  and dbgapi never reports the missing-memory error.

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='WaveDebugTest.Cwsr*'`: 3/3 passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `emulation/mirage/target/debug/mirage run --profile mi350x --gdb -- /bin/true`: returned `command not found: rocgdb` because the venv ROCgdb was not yet discoverable on `PATH` at this step; final-stack real ROCgdb coverage is recorded in row 21.
…trap mask

Validate the wave-launch trap-override request and report an accurate
device-supported trap mask, mirroring the real KFD contract.

`KFD_IOC_DBG_TRAP_SET_WAVE_LAUNCH_OVERRIDE` is how rocm-dbgapi enables the
address-watch exception trap and, at attach, discovers which exception
traps the device supports. The emulator echoed the request without
validation and reported a supported mask of 0 ("no traps supported"),
inconsistent with the hardware contract and misinforming any debugger that
relies on the reported mask before enabling watchpoints.

The real KFD validates in two layers: `kfd_dbg_validate_trap_override_request`
(amdgpu kfd_debug.c) starts from an all-ones supported mask, and the gfx9
backend `kgd_gfx_v9_validate_trap_override_request` (amdgpu_amdkfd_gfx_v9.c)
narrows it to `KFD_DBG_TRAP_MASK_DBG_ADDRESS_WATCH` and rejects any override
mode other than `OVERRIDE_OR` with `-EINVAL` (the `SPI_GDBG_TRAP_MASK`
register is global, so `REPLACE` could disturb other processes). The caller
then returns `-EACCES` when the requested support mask is not a subset of the
supported mask.
The `SET_WAVE_LAUNCH_OVERRIDE` handler now: rejects non-OR modes with
`-EINVAL`; rejects unsupported requested bits with `-EACCES`; records only
supported bits as enabled; and returns the previous enablement in
`enable_mask` and the supported mask (`DBG_ADDRESS_WATCH`) in
`support_request_mask`. Constants come from the vendored UAPI
(`KFD_DBG_TRAP_OVERRIDE_OR`, `KFD_DBG_TRAP_MASK_DBG_ADDRESS_WATCH`).
Reports the gfx9.4 supported mask (address-watch only); other gfx targets'
masks are not modeled. Enabling the bit is validated here; the watchpoint
trap itself is wired up in a following change.

Prototype of #8375.

- Unit: new `KfdIoctlTest.DbgTrapWaveLaunchOverrideValidatesRequest`
  (REPLACE -> EINVAL; unsupported bit -> EACCES; supported request ->
  success with correct previous/supported masks; second call sees the prior
  enablement). `DbgTrapAttachDetachConfigOps` strengthened to assert the
  supported mask. Full unit suite.
- E2E: `mirage run -- rocgdb` break/run/`info agents`/continue still clean.

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='KfdIoctlTest.DbgTrapWaveLaunchOverrideValidatesRequest:KfdIoctlTest.DbgTrapAttachDetachConfigOpsValidateAndResetState'`: 2/2 passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `emulation/mirage/target/debug/mirage run --profile mi350x --gdb -- /bin/true`: returned `command not found: rocgdb` because the venv ROCgdb was not yet discoverable on `PATH` at this step; final-stack real ROCgdb coverage is recorded in row 21.
Program the gfx9 four-slot address-watch model end to end so ROCgdb
hardware data watchpoints (watch/rwatch/awatch) trap the GPU wave that
accesses a watched address.

ROCgdb hardware data watchpoints on GPU memory did nothing on the
emulator: SET_NODE_ADDRESS_WATCH accepted the request but always returned
watch id 0 and never trapped an accessing wave, and the device snapshot
advertised zero watch registers, so a watchpoint could be created but
never inserted or fired. Data watchpoints are a core debugger capability
for catching who writes a buffer.

gfx9 has four TCP_WATCH slots; each holds an address, a compared-bit mask,
and an access mode. rocm-dbgapi learns how many watch registers the device
has from HSA_CAP_WATCH_POINTS_SUPPORTED + TOTALBITS in the agent
capability, programs a slot via SET_NODE_ADDRESS_WATCH, and the hardware
traps a wave whose access hits a watched address, setting
TRAPSTS.addr_watch<slot>.
- Advertise the four watch registers: `debug_topology_for` now sets
  HSA_CAP_WATCH_POINTS_SUPPORTED + TOTALBITS = ilog2(4) so the sysfs
  topology and GET_DEVICE_SNAPSHOT report a non-zero watch-register count.
  develop's topology refactor had dropped this; without it rocm-dbgapi
  refuses to insert a watchpoint ("too many hardware watchpoints").
- `DebugSession` stores four hardware watch slots (address/mask/mode).
  SET_NODE_ADDRESS_WATCH allocates a free slot and returns its id
  (-ENOMEM when all four are used); CLEAR_NODE_ADDRESS_WATCH frees it.
- The compute unit gained a `WatchpointHandler`, invoked from
  `issue_instruction` for each active-lane global-memory access. The probe
  is captured before the pipeline consumes the instruction and the match
  test runs after the PC advances, so a wave stopped on a watchpoint
  resumes past the watched access instead of re-triggering it.
- `on_wave_watchpoint` matches an access when the address block
  {X : (X & mask) == (address & mask)} overlaps the access span and the
  mode matches (READ / NONREAD write / ATOMIC / ALL). On a hit it raises
  TRAPSTS.addr_watch<slot> (gfx9: watch0@7, watch1..3@12/13/14) and
  MODE.excp_en.addr_watch (@19), then reports the wave through the trap
  path. All bit positions are named constants with citations.
gfx9 four-slot model (MI300/MI350). Watchpoint stop location is
instruction-accurate but not sub-instruction precise (ROCgdb notes
"precise memory signal reporting is not enabled").

Prototype of #8376.

- Unit: new KfdIoctlTest.DbgTrapNodeAddressWatchAllocatesAndFreesSlots
  (four distinct slots, ENOMEM on the fifth, freed slot reused,
  out-of-range clear rejected); the device-snapshot test asserts the
  watch-register capability. Full unit suite.
- E2E: debug_kernel.sh (CTest RocgdbDebug) gains a scenario that reads the
  device buffer pointer at the launch line, sets a hardware watchpoint on
  it from GPU context, and continues, asserting the watchpoint trips on the
  kernel's store (old/new value, stop at the store line). No hard-coded VA.

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='KfdIoctlTest.DbgTrapNodeAddressWatchAllocatesAndFreesSlots:WaveDebugTest.*'`: passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `emulation/mirage/target/debug/mirage run --profile mi350x --gdb -- /bin/true`: returned `command not found: rocgdb` because the venv ROCgdb was not yet discoverable on `PATH` at this step; final-stack real ROCgdb coverage is recorded in row 21.

emulation/rocjitsu/demos/rocgdb-watchpoint.{md,sh,cast}: catch which GPU wave writes a buffer with a hardware data watchpoint -- no AMD GPU required.
Raise an illegal-instruction exception when a wave fetches undecodable
code so ROCgdb stops at the faulting GPU PC instead of the wave silently
vanishing.

When an emulated wave fetched an undecodable instruction it was silently
halted and retired. Under a debugger this is confusing: the wave simply
disappears and the kernel "finishes" without the debugger ever learning
that bad code executed. Real hardware raises an illegal-instruction
exception that stops the wave at the faulting instruction.

rocm-dbgapi maps `TRAPSTS.illegal_inst` (gfx9 bit 11) to
`WAVE_STOP_REASON_ILLEGAL_INSTRUCTION` (architecture.cpp
signaled_exceptions) and drives its event loop from the queue's
`EC_QUEUE_WAVE_ILLEGAL_INSTRUCTION` exception (next_pending_event).
- The compute unit gained an `IllegalInstHandler`, invoked from the two
  places `issue_instruction` fails to decode (the `InvalidInst` catch and
  the null-decode guard). The PC is left at the faulting instruction (not
  advanced) so the debugger reports the illegal instruction's own address.
  When no debugger is attached the handler is unset and the wave halts as
  before, so stray bad code still cannot abort the emulator.
- `on_wave_illegal_instruction` sets `TRAPSTS.illegal_inst`, stops the
  wave, and reports an `EC_QUEUE_WAVE_ILLEGAL_INSTRUCTION` queue exception.
- `report_wave_stopped` gained an exception-mask parameter (default =
  `EC_QUEUE_WAVE_TRAP`) so the s_trap, single-step, watchpoint and
  illegal-instruction paths share one serialize-and-notify helper. The bit
  position and exception code are named and cited.
Memory-violation and math-error exceptions are separate changes: the
emulator's memory model auto-populates any address (no fault signal), so a
memory-violation model is larger and lands next.

Prototype of #8377.

- Unit: new `WaveDebugTest.IllegalInstructionStopsWaveUnderDebugger`
  (handler invoked, wave stopped at the faulting PC) and
  `IllegalInstructionWithoutDebuggerHalts` (no handler -> halts). Full suite.
- E2E: `debug_kernel.sh` (CTest `RocgdbDebug`) gains a scenario that
  single-steps to a clean instruction boundary, overwrites that instruction
  with `0xffffffff`, continues, and asserts ROCgdb reports SIGILL at the
  faulting GPU PC with no misaligned/corrupted-state error.

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='WaveDebugTest.IllegalInstruction*'`: 2/2 passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `emulation/mirage/target/debug/mirage run --profile mi350x --gdb -- /bin/true`: returned `command not found: rocgdb` because the venv ROCgdb was not yet discoverable on `PATH` at this step; final-stack real ROCgdb coverage is recorded in row 21.
Fault a wave that dereferences an unmapped device address and report it to
ROCgdb as SIGSEGV, instead of silently servicing the wild access from
sparse backing store.

A GPU wave that dereferenced an unmapped device address was silently
serviced from sparse backing store, so a wild/out-of-bounds access looked
like a normal (zeroed) access and the debugger never learned about it. Real
hardware raises a memory-access fault that stops the wave. With
illegal-instruction reporting this completes precise-exception handling for
the two faults a memory model can detect.

rocm-dbgapi maps `TRAPSTS.xnack_error` (gfx9 bit 28), not mode-gated, to
`WAVE_STOP_REASON_MEMORY_VIOLATION` (architecture.cpp signaled_exceptions),
and ROCgdb surfaces the queue's `EC_QUEUE_WAVE_MEMORY_VIOLATION` exception
as SIGSEGV.
- The compute unit gained a `MemoryViolationHandler`, invoked from
  `issue_instruction` for each active-lane global access whose address is
  not backed by any mapping (`GpuMemory::resolve_host_ptr` returns null). A
  memory fault outranks a watchpoint on the same instruction. The probe
  reuses the per-access address capture and runs after the PC advances.
- `on_wave_memory_violation` sets `TRAPSTS.xnack_error` and reports an
  `EC_QUEUE_WAVE_MEMORY_VIOLATION` queue exception. Bit and code are cited.
- Efficiency: the per-access watchpoint and memory-violation checks are now
  gated by a per-CU `debug_active_` flag the driver sets only while a debug
  session exists (ENABLE -> true, last DISABLE -> false), so undebugged
  execution pays no per-access debug cost. Previously the watchpoint capture
  ran for every global access unconditionally.
Detects unmapped-address faults; misaligned/permission faults and math-error
(FP-status) exceptions are not modeled. Stop location is imprecise (ROCgdb
notes "precise memory violation signal reporting is not enabled").

Prototype of #8378.

- Full unit suite (the `debug_active_` gate must not regress non-debug
  execution).
- E2E: `debug_kernel.sh` (CTest `RocgdbDebug`) gains a scenario that runs a
  new `bad_access.hip` kernel (store through a wild pointer) and asserts
  ROCgdb reports SIGSEGV in the faulting kernel; the four existing scenarios
  (well-formed add_one) confirm no false-positive violations.

- `cmake --build build --target rocjitsu_tests --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='WaveDebugTest.*'`: passed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `emulation/mirage/target/debug/mirage run --profile mi350x --gdb -- /bin/true`: returned `command not found: rocgdb` because the venv ROCgdb was not yet discoverable on `PATH` at this step; final-stack real ROCgdb coverage is recorded in row 21.

emulation/rocjitsu/demos/rocgdb-fault.{md,sh,cast}: catch a GPU memory-access fault (SIGSEGV) with ROCgdb -- no AMD GPU required.
… variables

Reproduce the hardware dword-interleaved scratch layout and publish the
per-wave scratch base so ROCgdb resolves scratch-resident kernel arguments
and locals (`print`, `info args`).

ROCgdb could inspect a stopped wave's registers, PC and disassembly, but
scratch-resident kernel arguments and locals (e.g. `data`, `n`) reported
"Cannot access memory at address private_lane#...". This was the last gap
preventing ordinary source-level debugging: `print`, `info args`, and
inspecting any spilled variable failed.

rocm-dbgapi reads private memory with the hardware dword-interleaved swizzle,
derives each wave's scratch base from the queue's COMPUTE_TMPRING_SIZE and
scratch_backing_memory_location, and cross-checks it against the wave's
architected FLAT_SCRATCH register.
- Swizzle scratch to the HW layout. `flat_calculate_addresses` (dedicated
  SCRATCH and the FLAT private-aperture path) maps a lane's private byte
  offset `off` to `base + (off/4)*lane_count*4 + lane*4 + off%4`, and the L1
  vector cache walks scratch with that per-dword stride. The interleave
  granule is a dword, matching rocdbgapi's private_lane interleave_size.
  Loads and stores share the layout, so execution stays self-consistent.
- Publish the queue scratch fields. At dispatch the CP writes
  scratch_backing_memory_location and COMPUTE_TMPRING_SIZE into the
  ABI-stable amd_queue_t, mirroring CP firmware; the shader-engines-per-XCC
  divisor is plumbed from the topology so dbgapi's scratch_memory_region does
  not disable private access.
- Serialize FLAT_SCRATCH. The CWSR serializer writes the wave's scratch base
  into flat_scratch's aliased SGPR slot (aliased_sgpr_end-6/-5, like VCC at
  -2), which dbgapi validates its computed base against (otherwise it prints
  "flat_scratch may be corrupted, private memory access is disabled").
gfx9 dword-interleaved private layout. Register/aperture derivation targets
the gfx9.4 CDNA scratch model.

Prototype of #8379.

- Full engine suite (`rocjitsu_tests`): the swizzle changes every scratch
  access, so this guards execution correctness. `ScratchAddrCalcTest` updated
  to assert the swizzled per-lane addresses and swizzle metadata.
- E2E: a new CTest RocgdbDebug scenario runs `print data`, `print n`,
  `print data[0]` and `info args` at a kernel breakpoint, asserting the
  values resolve with no "private_lane#" or "flat_scratch may be corrupted".

- `cmake --build build --target all --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='L1VectorCacheTest.*:ScratchAddrCalcTest.*:WaveDebugTest.Cwsr*'`: 12/12 passed.
- `./build/tests/rocjitsu_tests --gtest_filter='-*Benchmark*'`: 1675 passed, 1 skipped, 0 failed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- Real `mirage run --gdb` harness: passed private argument/pointer/value reads and all preceding breakpoint, step, watchpoint, SIGILL, and SIGSEGV scenarios.

emulation/rocjitsu/demos/rocgdb-quickstart.{md,sh,cast} now inspects the kernel's source-level arguments (`info args`, `print n`, `print data[0]`).
Correlate every trapped wave to its real dispatch and workgroup and read
each wave's own private memory, so ROCgdb can debug kernels with more than
one wave.

ROCgdb could debug only single-wave dispatches on the emulator. Any kernel
with more than one wave (a workgroup over 64 work-items, or more than one
workgroup) crashed rocm-dbgapi with "not in the same workgroup as the
group_leader" or "os_queue_packet_id ... is not within [...]". Real GPU
kernels are almost always multi-wave, so this made the debugger impractical.

Five pieces on top of the single-wave debugger:
1. Real dispatch correlation. Report `kfd_runtime_info.ttmp_setup = 1`, set
   each stopped wave's spi_ttmps_setup, and serialize the SPI TTMPs:
   workgroup coordinates (TTMP8-10), position in workgroup (TTMP11[0:5]) and
   the dispatch's AQL packet id (queue ring index at fetch, threaded through
   DispatchEntry and Wavefront) in TTMP11[6:30], which dbgapi validates
   against the queue's [read_dispatch_id, write_dispatch_id) range.
2. Read-pointer lifecycle. The CP now keeps a private fetch_cursor (so held
   packets are not re-fetched) and, on a wave trap, the driver holds
   read_dispatch_id at the oldest trapped dispatch's packet id; SUSPEND_QUEUES
   clears the context save area once those waves complete so dbgapi prunes the
   wave and dispatch cleanly at process exit. Non-debug execution is unchanged.
3. Per-workgroup control-stack marking. The serializer orders waves by
   (dispatch, group ids, position) and marks the first/last wave of each
   workgroup, instead of the first/last of the whole queue (which mis-grouped
   every wave after the first).
4. Atomic capture. `on_wave_trap` defers the debugger report until the queue
   quiesces (no running waves), so all stopped waves are serialized once in
   one stable snapshot; re-serializing after each trap would shift every
   wave's save-area address and defeat dbgapi's register cache. Single-wave
   dispatches quiesce immediately, so their behavior is unchanged.
5. Per-wave scratch. The CP tags each wave with its scratch_scoreboard_id and
   rounds the per-wave scratch stride to the 1 KB COMPUTE_TMPRING_SIZE.WAVESIZE
   granule; the serializer writes the scoreboard id into the wave's
   COMPUTE_RELAUNCH record so dbgapi computes each wave's scratch_base and
   private reads work for every wave.
gfx9.4 CDNA control-stack/scratch model. Very large dispatches are serialized
in one snapshot at trap time (bounded by the queue's wave capacity).

Prototype of #8380.

- Full engine suite (`rocjitsu_tests`): CP/scratch/serializer changes must not
  regress non-debug execution.
- E2E: a new `multi_wave.hip` (one workgroup, 128 threads = two waves) and a
  seventh CTest RocgdbDebug scenario that breaks inside the kernel, asserts
  `info threads` correlates both waves to workgroup (0,0,0) at positions /0
  and /1, and that each wave's scratch-resident `local` reads its own value
  (thread 0 -> 3, thread 64 -> 451), with no grouping/packet-id fault.

- `cmake --build build --target all --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='WaveDebugTest.Cwsr*:CommandProcessorTest.*'`: 4/4 passed.
- `./build/tests/rocjitsu_tests --gtest_filter='-*Benchmark*'`: 1675 passed, 1 skipped, 0 failed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- Real `mirage run --gdb` harness: both waves stopped and correlated to workgroup `(0,0,0)` positions `/0` and `/1`; private `local` values were 3 and 451; kernel completed with `h[0]=3 h[64]=451 h[127]=892`.

emulation/rocjitsu/demos/rocgdb-multiwave.{md,sh,cast}: debug a multi-wave GPU kernel and inspect each wave's private data.
Make the emulated GPU debugger discoverable now that it works end to end.

## Motivation
The ROCgdb debugging guide and the four recorded demos were only reachable
by knowing their paths. Linking them from the README index and collecting
the demos in the guide makes GPU debugging with mirage a first-class,
discoverable feature.

## Changes
- Add a "Debugging with ROCgdb" row to the README's Getting-started docs
  table.
- Add a Demos section to `rocgdb-debugging.md` listing the runnable
  quickstart / watchpoint / fault / multi-wave walkthroughs (each with a
  `.md`, `.sh`, and asciinema `.cast`), and how to regenerate a `.cast`.

Prototype of #8381.

## Test Plan
Documentation only; no code change. Markdown links verified against the
in-tree doc and demo paths.

## Test Result
- Markdown links verified against the in-tree guide and four demo triplets; section numbering remains sequential.
- `cmake --build build --target all --parallel "$(nproc)"`: passed.
- `./build/tests/rocjitsu_tests --gtest_filter='-*Benchmark*'`: 1675 passed, 1 skipped, 0 failed.
- `cargo build --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- `cargo test --workspace --manifest-path emulation/mirage/Cargo.toml`: passed.
- Real `mirage run --gdb` harness: passed all seven scenarios covering kernel breakpoints, three single-steps, displaced stepping, watchpoints, SIGILL, SIGSEGV, private variables, and two-wave correlation/private data.
The ROCgdb prototype previously converted s_trap directly into a host callback. That bypassed the trap base address registered by ROCr, so debugger-provided trap-handler code never executed and its architectural state transitions were not exercised.

- Resolve per-process, per-GPU TBA/TMA state and enter configured handler code from s_trap.
- Populate the gfx9.4 TTMP ABI and route TTMP operands to dedicated wave state.
- Implement the scalar return, send-message, and hardware-register operations used by the ROCr handler.
- Route doorbell and interrupt messages through KFD and publish CWSR only after the handler restores STATUS.HALT and returns.
- Keep trap handlers opaque to the host: no direct C++ breakpoint callback remains.
- Preserve debugger resume and single-step scheduling around handler execution.
- Exercise assembled handler code in wave, command-processor, and KFD integration tests.

- Build rocjitsu_shared and rocjitsu_tests.
- Run focused WaveDebug, KFD real-wave trap, and quiesced-CU scheduling tests.
- Run the non-benchmark rocjitsu suite.
- Run real ROCgdb breakpoint and stepping scenarios through Mirage.

- Build passed.
- 13 focused tests passed.
- 1675 non-benchmark tests passed with 1 skipped before final debugger-path refinements.
- Real ROCgdb reached the runtime-provided trap handler, stopped at the GPU breakpoint, and completed three single-step stops; post-continue validation still exposes a displaced-step register restore issue.
## Motivation

Executing the runtime-provided gfx9.4 trap handler exposed architectural state that the former host callback never exercised. ROCgdb could stop and single-step a wave, but continuing afterward corrupted helper-call targets, EXEC, and private-stack state, causing the wave to run away instead of completing.

## Technical Details

- Resolve linked rel32 SETPC/SWAPPC targets against each code object load bias while preserving absolute 48-bit GPU addresses.
- Preserve the interrupted EXEC mask across the ROCr handler doorbell exchange.
- Initialize and retain the CDNA private stack/frame SGPRs through CWSR write-back.
- Sign-extend scalar private-memory offsets and distinguish mapped GPU VAs from unavailable host translations.
- Keep generator output and generated CDNA3/CDNA4 instruction semantics synchronized.
- Discover Python ROCm SDK runtime libraries in the real ROCgdb harness.
- Add regressions for linked helper calls, trap EXEC restoration, and signed scratch offsets.

## Test Plan

- Build rocjitsu_shared and rocjitsu_tests.
- Run focused wave-debug, scratch, KFD trap, and quiesced-CU tests.
- Run the full non-benchmark rocjitsu test suite.
- Run cargo test --workspace in Mirage.
- Run all seven real ROCgdb scenarios through Mirage.

## Test Result

- Build passed.
- 18 focused tests passed.
- 1678 non-benchmark tests passed with 1 skipped.
- Mirage workspace tests and doc-tests passed.
- All seven real ROCgdb scenarios passed: breakpoint/step/continue, displaced stepping, watchpoint, SIGILL, SIGSEGV, private memory, and multi-wave correlation.
Linked gfx950 helper calls form their target from GETPC followed by a signed rel32 addition and SWAPPC. Resolve the encoded target against the GETPC address rather than the SWAPPC instruction itself, and align the focused regression with the four-instruction linked-call sequence.

Tested with two repeated real ROCgdb breakpoint/step/continue runs, all seven real ROCgdb scenarios, and the full non-benchmark rocjitsu suite (1678 passed, 1 skipped).
Classify enabled arithmetic traps in generated execute paths and propagate queue exceptions through the ROCr runtime-event ABI. Preserve stable CWSR state across queue suspension and resume, and accept all readable instruction mappings while rejecting true GPUVM faults.
Run each of the 89 gdb.rocm files in a bounded fresh daemon session, disable ROCjitsu backend logging, and record reproducible per-test summaries and backend hashes.
Keep illegal-instruction waves suspended when rocDBGAPI resumes their queue so ROCr can consume the fatal exception without the wave re-executing at the faulting PC. Track this lifecycle independently of debugger-restored architectural state and cover reset behavior.
Retain dispatch-relative workgroup coordinates on resident waves and publish the full tuple in CWSR records. Match restored records by the same tuple so rocDBGAPI reports correct dispatch and lane positions for multidimensional launches.
@amd-arosa
amd-arosa requested a review from kuhar August 18, 2026 19:51
The CWSR codec reproduces the gfx9.4 record layout only, so a wave stop on
any other part is declined. The topology still advertised
HSA_CAP_TRAP_DEBUG_SUPPORT for every debug-capable GPU, which is the bit
rocdbgapi keys agent_t::supports_debugging() on: a debugger could attach to
an agent the simulator cannot service and only discover the mismatch once
every individual stop came back declined.

Clear the bit for parts the codec does not model, in debug_topology_for()
and again in effective_topology_for() so a config that captured a real
device's capability word cannot smuggle it back. The descriptive
sub-capabilities stay driver-derived; they qualify a support that is no
longer claimed. cwsr_layout_modelled_for_gc_ip_version() sits beside the
arch-keyed predicate so the two spellings cannot drift apart.

Also fix the TSan failure in DbgTrapDebuggerExitReapsSessionAndAllowsReenable:
the reaper releases the debuggee after dropping debug_sessions_mutex_, so the
closed notifier fd is not a happens-before for the wave release. Sample the
wave under the CU's wave-state lock and poll for the release instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@amd-arosa

Copy link
Copy Markdown
Contributor Author

f3c7262

The TSan gate caught a data race in SimulationEngine::shutdown(): it clears
contexts_ and async_queues_ on the VM thread while the host thread is inside
request_exit(), which reads contexts_ and dereferences contexts_[0] to set the
partition's idle_wakeup_ flag. rj_daemon's teardown() drives exactly this
ordering, calling rj_vm_request_exit() before joining the engine thread, so a
self-terminating engine can be mid-shutdown when the exit request lands. The
result is a use-after-free on the partition context, not just a TSan warning.

Guard the three foreign-thread wake paths (request_exit, primary_release,
schedule_event_async) with a shared lock on a new contexts_mutex_, and take it
exclusively around the teardown in shutdown(). Readers do not exclude one
another, so async scheduling keeps its concurrency and only teardown blocks;
the duplicated wake bodies collapse into wake_partition(). shutdown() takes the
lock after joining the workers, so nothing it waits on can be holding it.

run() had the same shape of bug on a different member: it returned exit_status_
by value without holding exit_mutex_, racing request_exit()'s assignment to it
and copying the message string out from under the writer. Copy under the lock.

Adds TerminationTest.RequestExitRacingShutdownIsSafe, which reproduces both
races against the unfixed engine (TSan exit 66 at the same simulation.cpp lines
the gate reported) and is clean across 60 consecutive runs after the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@kuhar kuhar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but maybe get an approval from Tony before landing

@atgutier atgutier left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAJOR] emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/isa/arch/amdgpu/shared/addr_calc_flat.h:143 —
Mixed FLAT accesses apply scratch addressing to global lanes

Reasoning: FLAT routing is lane-specific, but encountering one private-aperture lane sets the
instruction-wide scratch_swizzle flag. The memory pipeline then passes a scratch stride for every
active lane. For a wave64 multi-dword operation, a global lane’s second dword is accessed at address

  • 256, rather than address + 4, whenever another lane targets scratch.

Evidence: memory_pipeline.cpp:497-505 forwards one stride for the whole request, and
l1_vector_cache.cpp:194-265 applies it to all lanes. Existing scratch tests cover uniformly private
lanes, not mixed private/global lanes.

Suggested fix: carry a per-lane scratch mask/stride, materialize per-element addresses, or split
private and global lanes into separate cache requests. Add mixed-lane flat_*_dwordx2 load and store
tests.

Confidence: high.

[MAJOR] emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/kmd/linux/simulated_kfd.cpp:3232 — CWSR resume
can restore into a recycled wave and expose a partially restored queue

Reasoning: resume_debug_queues() retains raw Wavefront* slot pointers after releasing their CU
locks, performs blocking CWSR reads, and later reads and mutates those slots without revalidating
their process, queue, or generation. The target can concurrently destroy its queue because cross-
process debug operations hold the debugger’s op_mutex_, not the target’s. Queue destruction resets
the permanent slot object, which can immediately be reused for another dispatch.

Additionally, apply_cwsr_to_wave() clears debug_suspended per wave. The engine can execute that wave
before the remaining waves and LDS records have been restored.

Evidence: unlocked matching and LDS restoration occur at 3296-3330; suspension is cleared at 3191-
3192. Queue destruction reaches command_processor.cpp:617-639, while wavefront.cpp:27-40 resets the
slot. The suspend failure rollback has the same retained-pointer problem at 3418-3457.

Suggested fix: keep all waves suspended until a transaction-wide commit, and identify waves by a
stable (CU, slot, process, queue, generation) descriptor that is revalidated under the CU lock
before every operation. Add destroy/reuse and multi-CU partial-restore race tests.

Confidence: high.

[MAJOR] emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/vm/amdgpu/gpu_memory.h:324 — SDMA copies
dereference translated pointers after dropping their lifetime locks

Reasoning: copy_block() calls translate(), which returns a borrowed host pointer after releasing
vmid_mutex_ and the page-table lock, and then performs memcpy. Concurrent process teardown can
unregister the VMID and unmap the allocation before that copy.

The command processor holds hw_queue_mutex_ while processing the SDMA ring, but process close
unregisters the memory and unmaps allocations at simulated_kfd.cpp:858-885 before synchronizing
through unregister_queue() at 900-905. Thus the CP can dereference unmapped storage.

Evidence: the existing resolve_host_ptr() documentation at gpu_memory.h:173-176 explicitly says
translated pointers require remapping and teardown to be quiesced. Normal reads and writes instead
keep the page-table lock through their callback.

Suggested fix: perform each source read and destination write through a locked mapping callback, as
read_mapped() and write_mapped() do. Also consider unregistering/stopping queues before backing-
memory teardown. Add an SDMA-copy-versus-process-close sanitizer regression.

Confidence: high.

[MAJOR] emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/kmd/linux/simulated_kfd.cpp:4069 — Old detach
cleanup can corrupt a replacement debug session

Reasoning: DISABLE erases the session, unlocks debug_sessions_mutex_, performs target-wide cleanup,
and only afterward records handshake cancellation. Another debugger can ENABLE the same target
during that interval. The old cleanup can then erase the new session’s events, resume its stopped
waves, reopen its queue gates, revoke its newly installed target-memory routing, and cancel its
runtime handshake. The debugger-death reaper has the same erase-then-unlocked-cleanup window.

There is also a lost-cancellation race without reattachment: the runtime handshake validates the
session at 3595-3606; detach records cancellation; then the handshake erases that cancellation at
3621-3625, fails to notify the now-removed session, and waits for the 60-second deadline.

Evidence: replacement ENABLE commits at 4036-4043; target-wide cleanup is at 3012-3074;
acknowledgements and cancellations are keyed only by PID in simulated_kfd.h:481-491.

Suggested fix: retain a releasing tombstone or session generation until cleanup completes.
Generation-tag cleanup, events, memory routing, acknowledgements, and cancellation, and register the
handshake waiter atomically with session validation.

Confidence: high.

[MAJOR] emulation/rocjitsu/lib/rocjitsu/src/rocjitsu/kmd/linux/simulated_kfd.cpp:3979 — Late failed
ENABLE double-closes the transferred notifier

Reasoning: daemon ENABLE adopts the transferred notifier into local sess.owned_dbg_fd before the
final target/debugger liveness checks at 4022-4027. A late failure destroys sess and closes the
descriptor. However, rj_vm.cpp:413-414 clears cmd->in_handle only when the ioctl succeeds, so
rj_daemon.cpp:421-424 leaves its ReceivedFds::notifier owning the same descriptor and closes it
again at request teardown.

Because daemon requests run concurrently, another thread can obtain the reused descriptor number
between those closes; the second close can then destroy an unrelated client socket, eventfd, or
transferred descriptor.

Suggested fix: defer notifier adoption until all failure-producing checks pass, or explicitly report
ownership transfer independently of the ioctl result and clear the transport handle whenever the
driver adopted it. Add an end-to-end late-liveness-failure test that verifies exactly one close.

Confidence: high.

amd-arosa and others added 8 commits August 19, 2026 18:14
…gdb/gold

Conflicts were all in emulation/rocjitsu, between develop's cluster-barrier
and decode-diagnostics work and this branch's ROCgdb support.

Resolutions:
- cdna5 vop3: develop refactored VBcnt onto the DecodeResult/emit_error
  decoder while this branch had moved it from vop3_alu.cpp to vop3_misc.cpp.
  Kept the relocation and ported develop's new-signature body, so no
  old-signature decode definition remains in the generated tree.
- TTMPs stay in the wave's trap-temporary file (set_ttmp) rather than the
  SGPR allocation, and TTMP8 joins 6/7/9 there. Constants become file
  indices, and develop's two new TTMP8 tests read wf.ttmp(8).
- Wavefront::halt keeps the CpCompletionNotice parameter alongside develop's
  named-barrier methods; release_wf keeps the deferred CP notification while
  gaining develop's barrier_wgs_ cleanup.
- cluster_wg_placements_ and cluster_barriers_ are guarded by one mutex.
  Keeping both would have left the same map under two locks. Kept
  cluster_placements_mutex_ for its documented ordering, and
  mark_cluster_workgroup_complete now collects barrier peers and LDS pins
  under it, then acts after unlocking.
- process_aql_packet takes both queue_packet_id and aql_packet_id; they are
  distinct fields in DispatchEntry.
- Undecodable instructions still route through illegal_inst_handler_, now
  reporting via develop's decode_error.message().

Also updated for develop's renames: ROCJITSU_CODE_ARCH_GFX1250 ->
ROCJITSU_CODE_ARCH_CDNA5 in wave_debug_test, the DecodeResult return in
instruction_execution_harness_test, and "arch":"gfx1250" -> "cdna5" in
config_test.

3336/3336 rocjitsu ctest tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FLAT routing is decided per lane against the private aperture, so a single
wave can mix scratch lanes with global ones. The address calculation set
scratch_swizzle and scratch_addr_stride on the whole instruction as soon as
any one lane landed in the private aperture, and the memory pipeline then
forwarded that stride for every active lane.

For a multi-dword FLAT access this put a global lane's second dword at
addr + wf_size*4 rather than addr + 4 whenever some other lane targeted
scratch. Uniform waves were unaffected, which is why the existing scratch
tests -- all of which use uniformly private lanes -- did not catch it.

Record which lanes were actually swizzled in scratch_lane_mask (equal to the
active mask for dedicated SCRATCH ops, where every lane is private) and issue
the private and global lanes as separate cache requests. Both L1 paths index
the register buffer by absolute lane, so splitting by mask leaves the data
layout unchanged.

Adds a mixed-aperture flat_store_dwordx2 regression test. Without the fix the
global lane's second dword is found at addr + 256 and the test fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
copy_block() resolved each endpoint with translate(), which drops both
vmid_mutex_ and the page-table lock before returning a borrowed host pointer,
and only then ran the memcpy. gpu_memory.h already documents that a translated
pointer is valid only while remapping and teardown are quiesced, and the SDMA
ring does not quiesce them: the command processor holds hw_queue_mutex_, while
process close unregisters memory and unmaps allocations before it synchronizes
through unregister_queue(). A close racing an in-flight SDMA packet could
therefore free the storage between resolution and the copy.

Add with_translated_span(), which applies exactly translate()'s resolution
rules -- page-bounded, contiguous, host-backed, addressable -- but hands the
pointer to a callback that runs while the mapping is still held, plus
copy_from_mapped()/copy_to_mapped() wrappers. copy_block() now uses those, so
the accept/reject decision for every chunk is unchanged (including the
passthrough and read_client_memory fallbacks) and no host pointer outlives the
lock that keeps its allocation alive.

This is the same discipline read_mapped()/write_mapped() already use for
ordinary reads and writes. The teardown ordering in simulated_kfd -- stopping
queues before releasing backing memory -- is worth tightening separately; this
change removes the dereference-after-unmap on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nger fail

Daemon-mode ENABLE moved the SCM_RIGHTS-transferred notifier into
sess.owned_dbg_fd before its final target and debugger liveness checks, on the
reasoning that a descriptor substituted into our fd space is ours on every
path. The transport does not agree: rj_vm.cpp clears cmd->in_handle only when
the ioctl returns 0, so on a late failure sess was destroyed (closing the fd)
and the transport then closed the same number again.

Daemon requests run concurrently, so another thread can be handed the reused
descriptor number between those two closes, and the second close then destroys
an unrelated client socket, eventfd, or transferred descriptor.

Adopt the notifier where the authorized memory fd is already adopted -- past
every failure check, immediately before the session is committed. Both
transferred fds now follow one rule: the driver takes them only on success, and
the caller reclaims them on every early return, which is exactly what the
transport's cmd->result == 0 gate implements. No transport change is needed.

Not covered by a new test: reaching the window needs daemon mode plus a target
or debugger that exits between the entry checks and the commit checks, which
there is no deterministic way to stage in the current harness. The existing
ENABLE tests are local mode, where the session never owns dbg_fd.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
runtime_debugger_handshake() validated the session under
debug_sessions_mutex_, released it, and only then cleared any stale ack or
cancellation for the pid. An explicit DBG_TRAP_DISABLE erases the session,
drops the same mutex, and calls cancel_runtime_handshake() afterwards, so a
detach landing in that gap recorded its cancellation before the clear ran --
and the clear then erased it.

Nothing was left to satisfy the wait, and the session it would have been
delivered to was already gone, so no acker remained either: the inferior sat
out the full 60-second liveness deadline on a detach that was supposed to
release it immediately.

Move the clear inside the same critical section as the lookup. A detach now
either erases the session before the lookup, in which case the handshake
returns early, or blocks on the mutex until after the clear, in which case its
cancellation lands on the wait -- which is what the existing comment already
claimed the ordering guaranteed. The lock order (debug_sessions_mutex_ then
runtime_handshake_mutex_) is the one the debugger-death reaper already uses.

The related reattachment hazard in the same area -- DISABLE erasing the
session, unlocking, and running target-wide cleanup while a replacement ENABLE
can commit a new session underneath it -- is not addressed here. It needs a
releasing tombstone or session generation so cleanup can tell its own state
from its successor's, which is a larger change than this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
apply_cwsr_to_wave() cleared debug_suspended as the last step of applying each
wave's record. Because resume_debug_queues() applies the records one wave at a
time, the first wave became runnable while its queue-mates -- and the LDS
images written in the same loop -- were still being restored, so the engine
could execute a wave against partially restored queue state.

The bit already had a natural commit point: the locked pass that follows the
restore loop sets debug_suspended for every wave in the queue, and its comment
already says the queue gate stays closed until all of them have consumed their
CWSR state. Leave the bit set in apply_cwsr_to_wave() and let that pass be the
only writer. Final state is unchanged on both the success and failure paths --
a rejected image still lands in the same pass, so a temporarily suspended wave
is not stranded.

Not addressed: resume_debug_queues() still holds raw Wavefront* and
ComputeUnitCore* across the blocking CWSR reads and the unlocked match loop,
so a queue destroyed concurrently by the target can recycle a slot underneath
them. Closing that needs waves identified by a revalidated
(CU, slot, process, queue, generation) descriptor rather than a pointer, which
is a larger change than this one and wants destroy/reuse and multi-CU
partial-restore tests alongside it.

Verified against the 3337-test rocjitsu ctest suite. The ROCgdb .exp suite,
which is what exercises this path end to end, is not in this repository and
was not run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gdb/gold

Develop moved 31 commits since the previous merge. GitHub had marked the PR
dirty, which is why no pull_request-event workflow would dispatch.

The substantive conflict is that both sides added a trailing optional parameter
to L1VectorCache::load/store: this branch's addr_stride, which carries the
scratch swizzle, and develop's element_lane_masks, which carries per-element
lane validity. They are independent features, so both parameters are kept and
the implementation composes them -- the swizzle path now materializes addresses
per element and skips elements a lane is not valid for, instead of striding
over them. With no element masks it walks exactly the lanes and bytes the
uniform path would. memory_pipeline forwards both, keeping the private/global
request split from the mixed-aperture fix.

Other resolutions:
- cdna5 addr_calc: keep develop's decode_buffer_resource() but read the SRD
  through read_scalar_selector() rather than read_sgpr(), so a descriptor
  sourced from TTMPs resolves against the trap-temporary file.
- config_test: develop's 9-CU-per-SE and drm_cu_active_number assertions are
  correct -- the config really is 8x4x9 -- so this branch's stale 8u goes, and
  its assign_queue_cp assertions stay.
- shared_infra_test: both sides' new L1 tests kept; develop's calls gain the
  addr_stride argument the merged signature inserts ahead of the element masks.

Two breakages inherited from develop, fixed here:
- xcd_distribution_test.cpp reads configs/gfx950_cdna4.json, which develop no
  longer ships -- it renamed the file to gfx950_mi355x.json in a different
  commit -- and its geometry constants still describe 8 CUs per SE when the
  config now has 9. The test fails on develop as it stands. Repointed at the
  current config and corrected to 4 SEs x 9 CUs, which leaves what it asserts
  unchanged.
- The same rename left this branch's assign_queue_cp(queue_ordinal) unmatched
  by develop's new no-arg call sites; each fixture is fresh, so the ordinals
  that reproduce the old rotation counter are 0, qi, 0.

3359/3359 rocjitsu ctest tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@therock-pr-bot

Copy link
Copy Markdown

Pre-commit check failed

⛔ pre-commit failed

Please run locally:

  • python -m pip install pre-commit
  • pre-commit install
  • pre-commit run --all-files --show-diff-on-failure

This repo uses .pre-commit-config.yaml.

The merged signature fits on one line once queue_packet_id and aql_packet_id
sit together, and the repo's pre-commit clang-format hook says so. This header
was resolved by hand during the first develop merge and missed the format sweep
that covered the other files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@atgutier
atgutier merged commit e5109e3 into develop Aug 19, 2026
53 checks passed
@atgutier
atgutier deleted the users/arosa/rj/rocgdb/gold branch August 19, 2026 22:12
atgutier pushed a commit that referenced this pull request Aug 19, 2026
## Motivation

Debugging a GPU kernel has required a physical AMD GPU. rocjitsu already
emulates
the AMD KFD closely enough to run real ROCr/HIP workloads, but it had no
debug
surface: `AMDKFD_IOC_DBG_TRAP` was unimplemented, waves could not be
stopped, and
there was no CWSR state for a debugger to read or write.

This PR closes that gap. **Real ROCgdb and real `librocm-dbgapi`,
unmodified, now
attach to a workload running on the emulated GPU and debug its kernels
end to end**,
with no AMD hardware present: breakpoints, single-step, watchpoints,
multi-wave
`info threads`, source-level locals out of private/scratch, SIGILL and
SIGSEGV on GPU
faults, and clean detach. One command drives it:

```bash
mirage run --profile mi350x --gdb -- ./my_hip_app
```

The bar this is measured against is the official ROCgdb `gdb.rocm` test
suite, which
is written for hardware and knows nothing about the emulator. All 89 of
its files
pass.

## Technical Details

All KFD debug behaviour is mirrored from the real driver source
(`amd/amdkfd/{kfd_chardev.c,kfd_debug.c,kfd_topology.c}`,
amdgpu-6.16.13),
cross-checked against a physical MI300X and against the in-tree
`projects/rocdbgapi` client. Changes are confined to `emulation/`.

**KFD debug protocol**
(`kmd/linux/{simulated_kfd,remote_driver,rj_daemon,interposer}`)

- `DBG_TRAP` sub-operation dispatcher with the driver's validation
ladder, authorized
by a *real* ptrace relationship and marshalled over the daemon. The
debugger and the
inferior are separate processes, so in-process mode cannot host this and
daemon mode
  is mandatory.
- Debug sessions keyed by inferior pid, so ROCgdb can attach before the
inferior ever
  opens `/dev/kfd`; full attach/detach lifecycle, `GET_DEVICE_SNAPSHOT`,
  `GET_QUEUE_SNAPSHOT`, `SET_EXCEPTIONS_ENABLED` / `SET_FLAGS`, and
  `SET_WAVE_LAUNCH_OVERRIDE` validated against the gfx9.4 trap mask.
- Debuggee memory access authorized through the daemon (debugger-opened
proc memory
and pinned process descriptors travel with the enable request),
preserving remote
`DONTFORK` semantics and naming the synthetic KFD marker as `/dev/kfd`
in proc-map
  snapshots so fork and `gcore` behave as they do on hardware.
- Runtime enable/disable handshake reported only to an attached
debugger, `-ESRCH`
(not `-EPERM`) once the target's KFD state is gone, and suspend/resume
of
already-destroyed queues answered so rocdbgapi can retire them during
teardown.

**Engine wave-stop and CWSR**
(`vm/amdgpu/{compute_unit,wavefront,command_processor}`,
`kmd/linux/cwsr.{h,cpp}`)

- Waves stop on `s_trap` for the KFD debugger; stopped-wave state is
serialized into
  the real CWSR area layout and written back on resume.
- The runtime's own gfx9.4 trap handler is executed rather than shimmed.
A wave frozen
inside it needs the *interrupted* architectural state published, not the
handler's,
so EXEC and STATUS are shadowed in both directions: a debugger's edit
lands on the
state `s_rfe` restores instead of the register the handler is still
running under.
- Deterministic single-step via `TRAPSTS.trap_after_inst`; reserved
debugger memory for
displaced stepping; GPU address watchpoints; illegal instruction
reported as SIGILL;
memory violation reported as SIGSEGV; private/scratch reads so `print`
and
  `info args` resolve.
- Multi-wave: atomic queue resume, wave reclaim on queue destroy,
scheduler-locked
residency, 3D workgroup coordinates in CWSR records, and gfx950 LDS
preservation.
- Bounded functional-mode scheduling quantum while debugging so active
CUs make fair
  progress without starving peer waves.

**ISA and memory fixes surfaced by the suite** (`isa/arch/amdgpu/`,
`vm/amdgpu/`)

Running real debugger workloads exercised paths nothing else had: linked
helper calls
resolved against the `GETPC` base, ALU exception classification in
generated execute
paths, scalar memory access faults, signed scratch offsets, unaligned
swizzled scratch,
cross-XCD atomic coherence, the `v_bcnt` accumulator, pageable host
copies through SDMA,
and `s_sleep`'s architected delay. Generator changes live in
`lib/python/amdisa/` with
the generated output regenerated, not hand-edited.

**Debugger UX, docs, demos.** `mirage run --gdb` / `--gdb-ex`;
`docs/rocgdb-debugging.md`; four recorded walkthroughs under `demos/`
(quickstart,
watchpoint, fault, multi-wave), each with a `.md` explainer, a runnable
`.sh`, and an
asciinema `.cast`.

**Test tooling.** `tools/run_rocgdb_official.py` runs each `gdb.rocm`
file in a fresh,
bounded, verified Mirage daemon session and records a per-test summary
plus backend
hashes; `tools/repeat_rocgdb_test.sh` repeats a subset serially under an
`flock` (two
concurrent Mirage sessions collide and manufacture failures) for chasing
intermittents.
Both archive the daemon's `host.log` alongside `gdb.log`. The emulated
KFD's
diagnostics land there and nowhere else, and three debugger defects
fixed in this PR
were invisible until it was collected.

## Issue Tracking

Epic: #8276

Closes #8364, closes #8365, closes #8366, closes #8367, closes #8368,
closes #8369,
closes #8370, closes #8371, closes #8372, closes #8373, closes #8374,
closes #8375,
closes #8376, closes #8377, closes #8378, closes #8379, closes #8380,
closes #8381.

| Item | Work |
|---|---|
| #8364 | Authorize `DBG_TRAP` by real ptrace + daemon transport |
| #8365 | Key debug sessions by inferior pid so rocgdb can attach |
| #8366 | Complete the `DBG_TRAP` attach/detach lifecycle |
| #8367 | Enumerate compute queues in `GET_QUEUE_SNAPSHOT` |
| #8368 | Stop a GPU wave on `s_trap` for the KFD debugger |
| #8369 | Serialize stopped-wave state into the CWSR area layout |
| #8370 | Raise a debug event and serialize the CWSR when a wave traps |
| #8371 | Drive GPU wave debugging end to end with ROCgdb |
| #8372 | `mirage run --gdb` one-command debugging |
| #8373 | Deterministic single-step via `TRAPSTS.trap_after_inst` |
| #8374 | Reserve debugger memory for step-over |
| #8375 | Validate `SET_WAVE_LAUNCH_OVERRIDE` against the gfx9.4 trap
mask |
| #8376 | GPU address watchpoints |
| #8377 | Illegal instruction reported as SIGILL |
| #8378 | Memory-access violation reported as SIGSEGV |
| #8379 | Private/scratch reads for `print` / `info args` |
| #8380 | Multi-wave GPU kernel debugging |
| #8381 | ROCgdb debugging guide and demos index |

Built on the already-merged foundations #8280, #8281, #8353 and #8363.

## Test Plan

1. **Official ROCgdb suite.** All 89 `gdb.rocm` test files, each in its
own fresh,
   bounded Mirage daemon session, run serially via
`tools/run_rocgdb_official.py --profile mi350x`. The suite is upstream
ROCgdb's own,
   written for hardware and unmodified.
2. **rocjitsu unit/integration suite.** Full non-benchmark `ctest` run,
including the 66
test cases added here (19 in the new `wave_debug_test.cpp`, 34 in
`kfd_ioctl_test.cpp`,
   the rest across shared-infra, VM, interposer and decode tests).
3. **End-to-end smoke test in CI.** New `RocgdbDebug.KernelBreakpoint`
ctest target drives
real ROCgdb through `mirage run` to set a kernel breakpoint, stop, read
wave registers,
and continue to a correct result. Gated on `rocgdb` and `hipcc` being
present; skips
   cleanly otherwise.

## Test Result

Both suites green at `c998c5cd` (branch tip):

| Suite | Result |
|---|---|
| Official ROCgdb `gdb.rocm` | **89/89 files pass, 0 FAIL**. 2376 PASS
over 2407 executed assertions (9 UNSUPPORTED, 7 KFAIL, 3 XFAIL, 6
WARNING, 6 UNTESTED) |
| rocjitsu `ctest` (non-benchmark) | **2492 tests, 0 failures** (4
skipped by design) |

## Submission Checklist

- [x] Look over the contributing guidelines at
https://github.com/ROCm/rocm-systems/blob/develop/CONTRIBUTING.md.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit e5109e3)
atgutier added a commit that referenced this pull request Aug 20, 2026
…t calls (#10444)

## Motivation

hipTensor and rocThrust gfx1250 objects are refused at code-object load
time with "indirect branch or call target recovery is not implemented
for relocated kernel text" on `s_swap_pc_i64`. A refused object leaves
HIP holding an invalid kernel
handle, so the failure surfaces as a segfault rather than a diagnostic.
The fix is upstream as #10376; this backports it to
`release/bkc/therock-10.1-20260811`.

## Commits Cherry-Picked

  | # | Upstream | PR | Backport | Subject |
  |---|----------|-----|----------|---------|
| 1 | `b6d838028e` | #10197 | `846fb16d12` | [rocjitsu][docs] Refresh
architecture support documentation |
| 2 | `5c8e33e391` | #10200 | `db5c58b1be` | [rocjitsu] Define
standalone decoder ownership |
| 3 | `be1f91998f` | #10199 | `ed102baf1c` | [rocjitsu] Apply async
IOFFSET to LDS addresses |
| 4 | `fda635e5b4` | — | `ad32ff7c18` | [rocjitsu] Extract per-scope
rollback and object-level checks from translate() |
| 5 | `8f75eb781e` | — | `4c0cb05207` | [rocjitsu] Emit leading V_NOPs
ahead of a gfx1250 MODE write |
| 6 | `00aa1e260f` | #9941 | `ded43b1787` | fix(rocjitsu): disable
zero-count TDM descriptors |
| 7 | `c810647609` | #10192 | `50ca7f1732` | [rocjitsu] Apply MUBUF
LDS-load instruction offset to the LDS address |
| 8 | `1d7866269d` | #9560 | `2e6bf26943` | [rocjitsu] code rewrite
condition validator |
| 9 | `95b6c410bb` | #10175 | `8418901076` | [rocjitsu] Fix
ds_read_b64_tr_b16 lane layout in gfx950 simulator |
| 10 | `bfaebcfb5c` | #10240 | `c4eac1e1f7` | [rocjitsu][util] Add
result and diagnostic utilities |
| 11 | `dbb7221fa7` | #9775 | `cdd0993a5f` | fix(rocjitsu): route tensor
DMA through wave VMID |
| 12 | `2779247222` | #9779 | `7bfeba6f5d` | feat(rocjitsu): Lazy vgpr
allocation system |
| 13 | `a659172a31` | #9936 | `180cf00325` | fix(rocjitsu): honor TDM
tensor bounds across iterations |
| 14 | `60c29efb39` | #10204 | `370bfb9796` | [rocjitsu] Fix gfx950
decoder extension handling |
| 15 | `c1dba564e7` | #10345 | `536110a083` | [rocjitsu] Model gfx1250
wave ABI state |
| 16 | `7f71be9f32` | #10346 | `3f922ad5f2` | [rocjitsu] Model gfx1250
barrier synchronization |
| 17 | `bea7ac8937` | #10368 | `7b5ecf6c7b` | [rocjitsu] Replace decoder
exceptions with results |
| 18 | `f2e878694e` | #10371 | `8cf750d23f` | [rocjitsu] Dispatch
operand validation by type |
| 19 | `dd619bed77` | #9735 | `1a612c47c0` | fix(rocjitsu): respect
same-wave LDS ordering |
| 20 | `c5fae8d2b2` | #10137 | `4360c4b978` | [rocjitsu] Rename gfx1250
to cdna5 for enums, configs, amdisa codegen helpers and generated
symbols. (NFC) |
| 21 | `b05a587f89` | #10274 | `bfb5f9fab0` | [rocjitsu] Align CDNA4
MI350X configs with captured topology |
| 22 | `f81f9d40db` | #10362 | `8177b5725c` | feat(rocjitsu): cdna5 OOB
behavior implementation |
| 23 | `53a939bd00` | #10283 | `141b109432` | [rocjitsu] add per-XCD
workgroup dispatch observability |
| 24 | `a0fb11bc17` | #10414 | `6bb4c5a959` | [rocjitsu] Refactor test
encoding handling in execution tests (NFC) |
| 25 | `e5109e315e` | #9844 | `322ac26b44` | feat(emulation): debug
emulated GPU kernels with real ROCgdb |
| 26 | `efa24f23c9` | — | `c3c9641073` | rocjitsu: Point the XCD
distribution test at the config that exists |
| 27 | `8b7763ef5e` | #10376 | `c3f419555a` | [rocjitsu] Translate
gfx1250 lane-banked and kernarg-supplied indirect calls |

## Issue Tracking

[ROCM-29436]

## Submission Checklist

- [x] Look over the contributing guidelines at
https://github.com/ROCm/rocm-systems/blob/develop/CONTRIBUTING.md.


[ROCM-29346]:
https://amd-hub.atlassian.net/browse/ROCM-29346?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

[ROCM-29436]:
https://amd-hub.atlassian.net/browse/ROCM-29436?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

---------

Signed-off-by: Tony Gutierrez <anthony.gutierrez@amd.com>
Signed-off-by: hanhanW <hanhan0912@gmail.com>
Co-authored-by: Jakub Kuderski <jakub@nod-labs.com>
Co-authored-by: James Newling <james.newling@gmail.com>
Co-authored-by: Fei Kou <feikou@meta.com>
Co-authored-by: Austin Lu <austin.lu@amd.com>
Co-authored-by: Fei Kou <feikou@anthropic.com>
Co-authored-by: Erick Ochoa Lopez <eochoalo@amd.com>
Co-authored-by: Han-Chung Wang <hanhan0912@gmail.com>
Co-authored-by: Rosa, Ada <arosa@amd.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
cmcknigh pushed a commit that referenced this pull request Aug 25, 2026
## Motivation

Debugging a GPU kernel has required a physical AMD GPU. rocjitsu already
emulates
the AMD KFD closely enough to run real ROCr/HIP workloads, but it had no
debug
surface: `AMDKFD_IOC_DBG_TRAP` was unimplemented, waves could not be
stopped, and
there was no CWSR state for a debugger to read or write.

This PR closes that gap. **Real ROCgdb and real `librocm-dbgapi`,
unmodified, now
attach to a workload running on the emulated GPU and debug its kernels
end to end**,
with no AMD hardware present: breakpoints, single-step, watchpoints,
multi-wave
`info threads`, source-level locals out of private/scratch, SIGILL and
SIGSEGV on GPU
faults, and clean detach. One command drives it:

```bash
mirage run --profile mi350x --gdb -- ./my_hip_app
```

The bar this is measured against is the official ROCgdb `gdb.rocm` test
suite, which
is written for hardware and knows nothing about the emulator. All 89 of
its files
pass.

## Technical Details

All KFD debug behaviour is mirrored from the real driver source
(`amd/amdkfd/{kfd_chardev.c,kfd_debug.c,kfd_topology.c}`,
amdgpu-6.16.13),
cross-checked against a physical MI300X and against the in-tree
`projects/rocdbgapi` client. Changes are confined to `emulation/`.

**KFD debug protocol**
(`kmd/linux/{simulated_kfd,remote_driver,rj_daemon,interposer}`)

- `DBG_TRAP` sub-operation dispatcher with the driver's validation
ladder, authorized
by a *real* ptrace relationship and marshalled over the daemon. The
debugger and the
inferior are separate processes, so in-process mode cannot host this and
daemon mode
  is mandatory.
- Debug sessions keyed by inferior pid, so ROCgdb can attach before the
inferior ever
  opens `/dev/kfd`; full attach/detach lifecycle, `GET_DEVICE_SNAPSHOT`,
  `GET_QUEUE_SNAPSHOT`, `SET_EXCEPTIONS_ENABLED` / `SET_FLAGS`, and
  `SET_WAVE_LAUNCH_OVERRIDE` validated against the gfx9.4 trap mask.
- Debuggee memory access authorized through the daemon (debugger-opened
proc memory
and pinned process descriptors travel with the enable request),
preserving remote
`DONTFORK` semantics and naming the synthetic KFD marker as `/dev/kfd`
in proc-map
  snapshots so fork and `gcore` behave as they do on hardware.
- Runtime enable/disable handshake reported only to an attached
debugger, `-ESRCH`
(not `-EPERM`) once the target's KFD state is gone, and suspend/resume
of
already-destroyed queues answered so rocdbgapi can retire them during
teardown.

**Engine wave-stop and CWSR**
(`vm/amdgpu/{compute_unit,wavefront,command_processor}`,
`kmd/linux/cwsr.{h,cpp}`)

- Waves stop on `s_trap` for the KFD debugger; stopped-wave state is
serialized into
  the real CWSR area layout and written back on resume.
- The runtime's own gfx9.4 trap handler is executed rather than shimmed.
A wave frozen
inside it needs the *interrupted* architectural state published, not the
handler's,
so EXEC and STATUS are shadowed in both directions: a debugger's edit
lands on the
state `s_rfe` restores instead of the register the handler is still
running under.
- Deterministic single-step via `TRAPSTS.trap_after_inst`; reserved
debugger memory for
displaced stepping; GPU address watchpoints; illegal instruction
reported as SIGILL;
memory violation reported as SIGSEGV; private/scratch reads so `print`
and
  `info args` resolve.
- Multi-wave: atomic queue resume, wave reclaim on queue destroy,
scheduler-locked
residency, 3D workgroup coordinates in CWSR records, and gfx950 LDS
preservation.
- Bounded functional-mode scheduling quantum while debugging so active
CUs make fair
  progress without starving peer waves.

**ISA and memory fixes surfaced by the suite** (`isa/arch/amdgpu/`,
`vm/amdgpu/`)

Running real debugger workloads exercised paths nothing else had: linked
helper calls
resolved against the `GETPC` base, ALU exception classification in
generated execute
paths, scalar memory access faults, signed scratch offsets, unaligned
swizzled scratch,
cross-XCD atomic coherence, the `v_bcnt` accumulator, pageable host
copies through SDMA,
and `s_sleep`'s architected delay. Generator changes live in
`lib/python/amdisa/` with
the generated output regenerated, not hand-edited.

**Debugger UX, docs, demos.** `mirage run --gdb` / `--gdb-ex`;
`docs/rocgdb-debugging.md`; four recorded walkthroughs under `demos/`
(quickstart,
watchpoint, fault, multi-wave), each with a `.md` explainer, a runnable
`.sh`, and an
asciinema `.cast`.

**Test tooling.** `tools/run_rocgdb_official.py` runs each `gdb.rocm`
file in a fresh,
bounded, verified Mirage daemon session and records a per-test summary
plus backend
hashes; `tools/repeat_rocgdb_test.sh` repeats a subset serially under an
`flock` (two
concurrent Mirage sessions collide and manufacture failures) for chasing
intermittents.
Both archive the daemon's `host.log` alongside `gdb.log`. The emulated
KFD's
diagnostics land there and nowhere else, and three debugger defects
fixed in this PR
were invisible until it was collected.

## Issue Tracking

Epic: #8276

Closes #8364, closes #8365, closes #8366, closes #8367, closes #8368,
closes #8369,
closes #8370, closes #8371, closes #8372, closes #8373, closes #8374,
closes #8375,
closes #8376, closes #8377, closes #8378, closes #8379, closes #8380,
closes #8381.

| Item | Work |
|---|---|
| #8364 | Authorize `DBG_TRAP` by real ptrace + daemon transport |
| #8365 | Key debug sessions by inferior pid so rocgdb can attach |
| #8366 | Complete the `DBG_TRAP` attach/detach lifecycle |
| #8367 | Enumerate compute queues in `GET_QUEUE_SNAPSHOT` |
| #8368 | Stop a GPU wave on `s_trap` for the KFD debugger |
| #8369 | Serialize stopped-wave state into the CWSR area layout |
| #8370 | Raise a debug event and serialize the CWSR when a wave traps |
| #8371 | Drive GPU wave debugging end to end with ROCgdb |
| #8372 | `mirage run --gdb` one-command debugging |
| #8373 | Deterministic single-step via `TRAPSTS.trap_after_inst` |
| #8374 | Reserve debugger memory for step-over |
| #8375 | Validate `SET_WAVE_LAUNCH_OVERRIDE` against the gfx9.4 trap
mask |
| #8376 | GPU address watchpoints |
| #8377 | Illegal instruction reported as SIGILL |
| #8378 | Memory-access violation reported as SIGSEGV |
| #8379 | Private/scratch reads for `print` / `info args` |
| #8380 | Multi-wave GPU kernel debugging |
| #8381 | ROCgdb debugging guide and demos index |

Built on the already-merged foundations #8280, #8281, #8353 and #8363.

## Test Plan

1. **Official ROCgdb suite.** All 89 `gdb.rocm` test files, each in its
own fresh,
   bounded Mirage daemon session, run serially via
`tools/run_rocgdb_official.py --profile mi350x`. The suite is upstream
ROCgdb's own,
   written for hardware and unmodified.
2. **rocjitsu unit/integration suite.** Full non-benchmark `ctest` run,
including the 66
test cases added here (19 in the new `wave_debug_test.cpp`, 34 in
`kfd_ioctl_test.cpp`,
   the rest across shared-infra, VM, interposer and decode tests).
3. **End-to-end smoke test in CI.** New `RocgdbDebug.KernelBreakpoint`
ctest target drives
real ROCgdb through `mirage run` to set a kernel breakpoint, stop, read
wave registers,
and continue to a correct result. Gated on `rocgdb` and `hipcc` being
present; skips
   cleanly otherwise.

## Test Result

Both suites green at `c998c5cd` (branch tip):

| Suite | Result |
|---|---|
| Official ROCgdb `gdb.rocm` | **89/89 files pass, 0 FAIL**. 2376 PASS
over 2407 executed assertions (9 UNSUPPORTED, 7 KFAIL, 3 XFAIL, 6
WARNING, 6 UNTESTED) |
| rocjitsu `ctest` (non-benchmark) | **2492 tests, 0 failures** (4
skipped by design) |

## Submission Checklist

- [x] Look over the contributing guidelines at
https://github.com/ROCm/rocm-systems/blob/develop/CONTRIBUTING.md.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ajanicijamd pushed a commit that referenced this pull request Aug 25, 2026
## Motivation

Debugging a GPU kernel has required a physical AMD GPU. rocjitsu already
emulates
the AMD KFD closely enough to run real ROCr/HIP workloads, but it had no
debug
surface: `AMDKFD_IOC_DBG_TRAP` was unimplemented, waves could not be
stopped, and
there was no CWSR state for a debugger to read or write.

This PR closes that gap. **Real ROCgdb and real `librocm-dbgapi`,
unmodified, now
attach to a workload running on the emulated GPU and debug its kernels
end to end**,
with no AMD hardware present: breakpoints, single-step, watchpoints,
multi-wave
`info threads`, source-level locals out of private/scratch, SIGILL and
SIGSEGV on GPU
faults, and clean detach. One command drives it:

```bash
mirage run --profile mi350x --gdb -- ./my_hip_app
```

The bar this is measured against is the official ROCgdb `gdb.rocm` test
suite, which
is written for hardware and knows nothing about the emulator. All 89 of
its files
pass.

## Technical Details

All KFD debug behaviour is mirrored from the real driver source
(`amd/amdkfd/{kfd_chardev.c,kfd_debug.c,kfd_topology.c}`,
amdgpu-6.16.13),
cross-checked against a physical MI300X and against the in-tree
`projects/rocdbgapi` client. Changes are confined to `emulation/`.

**KFD debug protocol**
(`kmd/linux/{simulated_kfd,remote_driver,rj_daemon,interposer}`)

- `DBG_TRAP` sub-operation dispatcher with the driver's validation
ladder, authorized
by a *real* ptrace relationship and marshalled over the daemon. The
debugger and the
inferior are separate processes, so in-process mode cannot host this and
daemon mode
  is mandatory.
- Debug sessions keyed by inferior pid, so ROCgdb can attach before the
inferior ever
  opens `/dev/kfd`; full attach/detach lifecycle, `GET_DEVICE_SNAPSHOT`,
  `GET_QUEUE_SNAPSHOT`, `SET_EXCEPTIONS_ENABLED` / `SET_FLAGS`, and
  `SET_WAVE_LAUNCH_OVERRIDE` validated against the gfx9.4 trap mask.
- Debuggee memory access authorized through the daemon (debugger-opened
proc memory
and pinned process descriptors travel with the enable request),
preserving remote
`DONTFORK` semantics and naming the synthetic KFD marker as `/dev/kfd`
in proc-map
  snapshots so fork and `gcore` behave as they do on hardware.
- Runtime enable/disable handshake reported only to an attached
debugger, `-ESRCH`
(not `-EPERM`) once the target's KFD state is gone, and suspend/resume
of
already-destroyed queues answered so rocdbgapi can retire them during
teardown.

**Engine wave-stop and CWSR**
(`vm/amdgpu/{compute_unit,wavefront,command_processor}`,
`kmd/linux/cwsr.{h,cpp}`)

- Waves stop on `s_trap` for the KFD debugger; stopped-wave state is
serialized into
  the real CWSR area layout and written back on resume.
- The runtime's own gfx9.4 trap handler is executed rather than shimmed.
A wave frozen
inside it needs the *interrupted* architectural state published, not the
handler's,
so EXEC and STATUS are shadowed in both directions: a debugger's edit
lands on the
state `s_rfe` restores instead of the register the handler is still
running under.
- Deterministic single-step via `TRAPSTS.trap_after_inst`; reserved
debugger memory for
displaced stepping; GPU address watchpoints; illegal instruction
reported as SIGILL;
memory violation reported as SIGSEGV; private/scratch reads so `print`
and
  `info args` resolve.
- Multi-wave: atomic queue resume, wave reclaim on queue destroy,
scheduler-locked
residency, 3D workgroup coordinates in CWSR records, and gfx950 LDS
preservation.
- Bounded functional-mode scheduling quantum while debugging so active
CUs make fair
  progress without starving peer waves.

**ISA and memory fixes surfaced by the suite** (`isa/arch/amdgpu/`,
`vm/amdgpu/`)

Running real debugger workloads exercised paths nothing else had: linked
helper calls
resolved against the `GETPC` base, ALU exception classification in
generated execute
paths, scalar memory access faults, signed scratch offsets, unaligned
swizzled scratch,
cross-XCD atomic coherence, the `v_bcnt` accumulator, pageable host
copies through SDMA,
and `s_sleep`'s architected delay. Generator changes live in
`lib/python/amdisa/` with
the generated output regenerated, not hand-edited.

**Debugger UX, docs, demos.** `mirage run --gdb` / `--gdb-ex`;
`docs/rocgdb-debugging.md`; four recorded walkthroughs under `demos/`
(quickstart,
watchpoint, fault, multi-wave), each with a `.md` explainer, a runnable
`.sh`, and an
asciinema `.cast`.

**Test tooling.** `tools/run_rocgdb_official.py` runs each `gdb.rocm`
file in a fresh,
bounded, verified Mirage daemon session and records a per-test summary
plus backend
hashes; `tools/repeat_rocgdb_test.sh` repeats a subset serially under an
`flock` (two
concurrent Mirage sessions collide and manufacture failures) for chasing
intermittents.
Both archive the daemon's `host.log` alongside `gdb.log`. The emulated
KFD's
diagnostics land there and nowhere else, and three debugger defects
fixed in this PR
were invisible until it was collected.

## Issue Tracking

Epic: #8276

Closes #8364, closes #8365, closes #8366, closes #8367, closes #8368,
closes #8369,
closes #8370, closes #8371, closes #8372, closes #8373, closes #8374,
closes #8375,
closes #8376, closes #8377, closes #8378, closes #8379, closes #8380,
closes #8381.

| Item | Work |
|---|---|
| #8364 | Authorize `DBG_TRAP` by real ptrace + daemon transport |
| #8365 | Key debug sessions by inferior pid so rocgdb can attach |
| #8366 | Complete the `DBG_TRAP` attach/detach lifecycle |
| #8367 | Enumerate compute queues in `GET_QUEUE_SNAPSHOT` |
| #8368 | Stop a GPU wave on `s_trap` for the KFD debugger |
| #8369 | Serialize stopped-wave state into the CWSR area layout |
| #8370 | Raise a debug event and serialize the CWSR when a wave traps |
| #8371 | Drive GPU wave debugging end to end with ROCgdb |
| #8372 | `mirage run --gdb` one-command debugging |
| #8373 | Deterministic single-step via `TRAPSTS.trap_after_inst` |
| #8374 | Reserve debugger memory for step-over |
| #8375 | Validate `SET_WAVE_LAUNCH_OVERRIDE` against the gfx9.4 trap
mask |
| #8376 | GPU address watchpoints |
| #8377 | Illegal instruction reported as SIGILL |
| #8378 | Memory-access violation reported as SIGSEGV |
| #8379 | Private/scratch reads for `print` / `info args` |
| #8380 | Multi-wave GPU kernel debugging |
| #8381 | ROCgdb debugging guide and demos index |

Built on the already-merged foundations #8280, #8281, #8353 and #8363.

## Test Plan

1. **Official ROCgdb suite.** All 89 `gdb.rocm` test files, each in its
own fresh,
   bounded Mirage daemon session, run serially via
`tools/run_rocgdb_official.py --profile mi350x`. The suite is upstream
ROCgdb's own,
   written for hardware and unmodified.
2. **rocjitsu unit/integration suite.** Full non-benchmark `ctest` run,
including the 66
test cases added here (19 in the new `wave_debug_test.cpp`, 34 in
`kfd_ioctl_test.cpp`,
   the rest across shared-infra, VM, interposer and decode tests).
3. **End-to-end smoke test in CI.** New `RocgdbDebug.KernelBreakpoint`
ctest target drives
real ROCgdb through `mirage run` to set a kernel breakpoint, stop, read
wave registers,
and continue to a correct result. Gated on `rocgdb` and `hipcc` being
present; skips
   cleanly otherwise.

## Test Result

Both suites green at `c998c5cd` (branch tip):

| Suite | Result |
|---|---|
| Official ROCgdb `gdb.rocm` | **89/89 files pass, 0 FAIL**. 2376 PASS
over 2407 executed assertions (9 UNSUPPORTED, 7 KFAIL, 3 XFAIL, 6
WARNING, 6 UNTESTED) |
| rocjitsu `ctest` (non-benchmark) | **2492 tests, 0 failures** (4
skipped by design) |

## Submission Checklist

- [x] Look over the contributing guidelines at
https://github.com/ROCm/rocm-systems/blob/develop/CONTRIBUTING.md.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
kuhar added a commit that referenced this pull request Oct 27, 2026
The CDNA5 descriptor tests in triton-lang/triton#12512 pass on
hardware but fail under Rocjitsu. Crossing raw B128 accesses load and
store one extra DWORD near NUM_RECORDS.

- CDNA5 buffer bounds (PR #10362, f81f9d4): retain complete
accesses, including exact fits, and apply the PR's stated end + 4 <=
NUM_RECORDS rule to crossing raw vector accesses. Keep SOFFSET in the
check and preserve structured and independent stride bounds.

- GFX12 atomic returns (PR #7336, 3a4e98a): test TH[0]
independently of the non-temporal and cascade bits. Returning modes 3,
5 and 7 otherwise omit the old value and select non-returning metadata
and wait counters.

- Signed scratch operands (CDNA5 PR #6673, f6aec50; RDNA4 PR
#5053, ca4b97e): sign-extend scalar and vector offsets before
combining them and before CDNA5 payload scaling. Compensated negative
operands otherwise become large positive private offsets and fault.

- CDNA GLOBAL operands (PR #5512, 5a107ce): zero-extend the
32-bit VGPR offset when a scalar base is present. High-bit offsets
otherwise resolve four GiB below the intended address. Preserve
absolute VGPR pairs and CDNA5 signed addressing. CDNA1-4 manuals and
LLVM agree on the unsigned rule.

- PC exports and targets (import handling #9844, e5109e3): apply
the architecture's extension rule to
GETPC, CALL and SWAPPC return addresses. RDNA4 zero-extends 48 bits;
older targets sign-extend 48 bits and CDNA5 sign-extends 57 bits.
Keep imported PCs as raw 48/57-bit addresses for instruction fetch,
consistent with RFE, and preserve all CDNA5 target bits. Limit the
positive linked-rel32 tag to 48-bit targets. Remove the negative
tag on every target: it aliases ordinary sign-filled PC exports.
Both forms otherwise corrupt valid 57-bit targets or older high-PC
round trips when a load bias is present.

For buffer bounds, the Triton PR measures aligned B128 crossings with
four or eight bytes remaining. Other widths and byte-granular bounds
follow its stated rule as a model choice pending gfx1250
qualification. Record that limit beside the implementation and
regression expectations.

Cover bounds, inactive lanes, signed and unsigned operand
combinations, atomic writeback and wait counters, including image
metadata. Validate 49 Gluon cases with matching ISA and 172 hardware
comparisons each on gfx1201 and gfx1100. Audit gfx942/gfx950 buffer,
scratch and atomic contracts against the manuals. The standard CTest
sweep retains the same 31 baseline failures.

Decode and execute PC regression cases on gfx1100, gfx1201, gfx950 and
gfx1250, including high address bits, next-PC boundaries and operand
aliasing. Follow SETPC/SWAPPC through real page tables to fetch a high
mapped destination, including both relocation-tag collisions with a
nonzero load bias. Older linked-rel32 debugger coverage is retained.
These are manual/LLVM-backed simulation checks without physical
high-address PC qualification. RDNA3/3.5 sign-filled exports follow
LLVM against conflicting manual tables; record that choice beside
the generator and test oracle. Add explicit test braces for GCC.
kuhar added a commit that referenced this pull request Oct 27, 2026
The CDNA5 descriptor tests in triton-lang/triton#12512 pass on
hardware but fail under Rocjitsu. Crossing raw B128 accesses load and
store one extra DWORD near NUM_RECORDS.

- CDNA5 buffer bounds (PR #10362, f81f9d4): retain complete
accesses, including exact fits, and apply the PR's stated end + 4 <=
NUM_RECORDS rule to crossing raw vector accesses. Keep SOFFSET in the
check and preserve structured and independent stride bounds.

- GFX12 atomic returns (PR #7336, 3a4e98a): test TH[0]
independently of the non-temporal and cascade bits. Returning modes 3,
5 and 7 otherwise omit the old value and select non-returning metadata
and wait counters.

- Signed scratch operands (CDNA5 PR #6673, f6aec50; RDNA4 PR
#5053, ca4b97e): sign-extend scalar and vector offsets before
combining them and before CDNA5 payload scaling. Compensated negative
operands otherwise become large positive private offsets and fault.

- CDNA GLOBAL operands (PR #5512, 5a107ce): zero-extend the
32-bit VGPR offset when a scalar base is present. High-bit offsets
otherwise resolve four GiB below the intended address. Preserve
absolute VGPR pairs and CDNA5 signed addressing. CDNA1-4 manuals and
LLVM agree on the unsigned rule.

- PC exports and targets (import handling #9844, e5109e3): apply
the architecture's extension rule to
GETPC, CALL and SWAPPC return addresses. RDNA4 zero-extends 48 bits;
older targets sign-extend 48 bits and CDNA5 sign-extends 57 bits.
Keep imported PCs as raw 48/57-bit addresses for instruction fetch,
consistent with RFE, and preserve all CDNA5 target bits. Limit the
positive linked-rel32 tag to 48-bit targets. Remove the negative
tag on every target: it aliases ordinary sign-filled PC exports.
Both forms otherwise corrupt valid 57-bit targets or older high-PC
round trips when a load bias is present.

For buffer bounds, the Triton PR measures aligned B128 crossings with
four or eight bytes remaining. Other widths and byte-granular bounds
follow its stated rule as a model choice pending gfx1250
qualification. Record that limit beside the implementation and
regression expectations.

Cover bounds, inactive lanes, signed and unsigned operand
combinations, atomic writeback and wait counters, including image
metadata. Validate 49 Gluon cases with matching ISA and 172 hardware
comparisons each on gfx1201 and gfx1100. Audit gfx942/gfx950 buffer,
scratch and atomic contracts against the manuals. The standard CTest
sweep retains the same 31 baseline failures.

Decode and execute PC regression cases on gfx1100, gfx1201, gfx950 and
gfx1250, including high address bits, next-PC boundaries and operand
aliasing. Follow SETPC/SWAPPC through real page tables to fetch a high
mapped destination, including both relocation-tag collisions with a
nonzero load bias. Older linked-rel32 debugger coverage is retained.
These are manual/LLVM-backed simulation checks without physical
high-address PC qualification. RDNA3/3.5 sign-filled exports follow
LLVM against conflicting manual tables; record that choice beside
the generator and test oracle. Add explicit test braces for GCC.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment