Files
TheRock/rocm-systems/projects/rocshmem/scripts/debug/README.md
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

18 KiB

rocSHMEM Deadlock Analysis Tools

Scripts for identifying and diagnosing deadlocks in rocSHMEM applications using AMD's ROCm debugger (rocgdb).

Files

File Purpose
rocgdb_deadlock_analysis.py rocgdb Python script: coalesces GPU wavefront backtraces, identifies rocSHMEM API entry points, and provides deadlock hints
attach_deadlock_analysis.sh Shell wrapper: finds all running instances of an executable on the local node, attaches to each, saves per-process output, prints a compact summary
mpiexec_deadlock_analysis.sh Multi-node wrapper: deploys attach_deadlock_analysis.sh to every node via mpiexec, then runs cross-rank coalescing
cross_rank_deadlock_analysis.py Cross-rank coalescer: reads per-rank output files, groups identical backtrace patterns across ranks, highlights patterns uncommon among ranks
deadlock_test.cc Intentional-deadlock test program used to validate the scripts

Prerequisites

  • rocgdb in PATH (provided by ROCm; override path with ROCGDB env var)
  • timeout command (standard on Linux)
  • Python support in rocgdb (built-in for ROCm 6+)

For meaningful backtraces the application and rocSHMEM library should be built with debug symbols. A RelWithDebInfo build (CMake -DCMAKE_BUILD_TYPE=RelWithDebInfo) gives full inlined call chains while keeping optimized performance. A Release build will still show the outermost non-inlined API frame but not the internal wait loops.


Quick Start

# 1. Start your (deadlocked) application
mpirun -np 4 ./my_rocshmem_app &

# 2. Attach to all instances, analyze, save output
./scripts/debug/attach_deadlock_analysis.sh my_rocshmem_app

# Output is written to ./rocshmem_deadlock_analysis_<timestamp>/
# A compact summary is printed to stdout.

attach_deadlock_analysis.sh

Usage

attach_deadlock_analysis.sh <executable_name> [--directory <dir>] [--cull[=p1,p2,...]]
Argument Description
executable_name Exact process name to search for
--directory <dir> Directory for per-process output files (default: ./rocshmem_deadlock_analysis_<timestamp>/)
--cull Cull groups stuck in GPU barriers / gridsync using the built-in default pattern list
--cull=p1,p2,... Cull groups whose backtrace contains any of the comma-separated substrings

Environment Variables

Variable Default Description
ROCGDB rocgdb Path to the rocgdb binary
ROCSHMEM_GDB_TIMEOUT 120 Per-process timeout in seconds
ROCSHMEM_DEADLOCK_COLOR always Color mode for output files (see Color Support)

Output Files

Each analyzed process produces a file named:

<output_dir>/pe<PE_RANK>_pid_<PID>.txt

The MPI PE rank is read from the process's OMPI_COMM_WORLD_RANK environment variable. If the variable is not set (non-MPI process or already exited), the rank is shown as unknown.

Stdout Summary

After all processes are analyzed, a compact summary is printed:

=== Attach and Analyze Summary ===
Processes analyzed: 2 / 2

  PE 0     PID 12345   groups=?    (no GPU threads found)
  PE 1     PID 12346   groups=1    Waiting for remote PE memory update...

Full output in: ./rocshmem_deadlock_analysis_20260416_143022/
  • groups — number of distinct backtrace groups found in that process
  • dominant — the most frequent [HINT] line across all groups, or a status string if no deadlock was detected

Error Handling

Situation Behavior
Process exits before attach Logged as SKIP, counted as failure
rocgdb exceeds timeout Logged as TIMEOUT; output file kept for inspection
rocgdb exits non-zero but produced analysis Still counted as success
rocgdb not found in PATH Fatal error with hint to set ROCGDB

rocgdb_deadlock_analysis.py

The Python script can be used in three modes.

Mode 1: Batch attach (used by attach_deadlock_analysis.sh)

Attach rocgdb to a running process and analyze immediately:

ROCSHMEM_DEADLOCK_AUTO_ANALYZE=1 \
    rocgdb -batch -p <pid> -x scripts/debug/rocgdb_deadlock_analysis.py

The ROCSHMEM_DEADLOCK_AUTO_ANALYZE=1 variable tells the script to run the analysis as soon as the process is stopped (which happens automatically on attach).

Mode 2: Interactive session

Attach rocgdb manually, then source and run the script:

rocgdb -p <pid>
(rocgdb) source scripts/debug/rocgdb_deadlock_analysis.py
(rocgdb) rocshmem-deadlock-analyze

Write the report to a file:

(rocgdb) rocshmem-deadlock-analyze /tmp/analysis.txt

Force colored output in the file:

(rocgdb) rocshmem-deadlock-analyze --color /tmp/analysis.txt

Mode 3: Launch mode

Run the application under rocgdb from the start; analysis triggers on first stop (e.g. pkill --signal SIGINT my_rocshmem_app), or when the program crashes:

mpirun -n 2 -x ROCSHMEM_DEADLOCK_AUTO_ANALYZE=1 rocgdb -batch \
    -x scripts/debug/rocgdb_deadlock_analysis.py \
    -ex run \
    --args ./my_rocshmem_app arg1 arg2

### GDB Command Reference

After sourcing the script, the `rocshmem-deadlock-analyze` command is available:

(rocgdb) rocshmem-deadlock-analyze [--color|--no-color] [--cull[=pat,...]] [--check-lanes] [output_file]


| Option | Description |
|---|---|
| `--color` | Force ANSI color codes on, even when writing to a file |
| `--no-color` | Disable color codes, even on a TTY |
| `--cull` | Cull groups stuck in GPU barriers / gridsync using the built-in default pattern list |
| `--cull=p1,p2,...` | Cull groups whose backtrace contains any of the comma-separated substrings |
| `--check-lanes` | Enable lane-level parameter mismatch detection for `_wg` and `_wave` collective calls (slow; see below) |
| `--stats` | Append a `=== rocSHMEM Stats ===` section with non-zero API call counters (requires `-DPROFILE=ON` build) |
| `output_file` | Write report to this file instead of stdout |

#### Cull option

When `--cull` is given without a pattern list, the following substrings are used to remove groups
that are simply waiting at a non-rocSHMEM GPU synchronization primitive (and are therefore not
rocSHMEM deadlocks):

| Pattern | Covers |
|---|---|
| `__syncthreads` | HIP workgroup barrier and reduction variants (`_count`, `_and`, `_or`) |
| `__builtin_amdgcn_s_barrier` | Direct AMDGCN scalar barrier intrinsic |
| `__work_group_barrier` | Workgroup barrier with memory-fence flags |
| `__named_sync` | Named barrier intrinsic |
| `__ockl_grid_sync` | OCKL grid-level synchronization |
| `__ockl_gws_barrier` | OCKL global-workspace barrier |
| `__ockl_multi_grid_sync` | OCKL multi-grid synchronization |
| `cooperative_groups` | HIP cooperative groups (`grid_group`, `thread_block`, `multi_grid_group`) |

Culled groups are not shown in the report; the header prints a count of how many
groups and wavefronts were removed:

Unique backtrace groups: 1 (+ 3 group(s) / 192 wavefront(s) culled — GPU barrier / gridsync)


### Environment Variables

| Variable | Default | Description |
|---|---|---|
| `ROCSHMEM_DEADLOCK_AUTO_ANALYZE` | `0` | Set to `1` to trigger analysis automatically on process stop |
| `ROCSHMEM_DEADLOCK_COLOR` | `always` | Color mode (see [Color Support](#color-support)) |

---

## Output Format

=== rocSHMEM Deadlock Analysis === Process(es): 12346 Total GPU wavefronts analyzed: 64 Unique backtrace groups: 2

--- Group 1 (63 wavefront(s)) --- WFs: (0,0,0)/0,(1,0,0)/0,(2,0,0)/0,... Backtrace: #0 rocshmem::uncached_load (...) at src/assembly.hpp:125 #1 rocshmem::Context::test (...) at src/context_tmpl_device.hpp:432 #2 rocshmem::Context::wait_until (...) at src/context_tmpl_device.hpp:224 #3 rocshmem::IPCContext::internal_direct_barrier (...) at ... ... #7 rocshmem::Context::barrier_all_wg (...) at src/context_device.cpp:151 #8 my_kernel () at my_app.cpp:42 [HINT] Waiting for remote PE memory update (barrier/sync or user rocshmem_wait_until). Check if the remote PE is alive and making progress.

--- Group 2 (1 wavefront(s)) --- WGs: (0,0,0) WFs: 0 Backtrace: #0 rocshmem::acquire_lock (...) at src/gda/mlx5/queue_pair_mlx5.cpp:53 #1 rocshmem::QueuePair::mlx5_post_wqe_rma (...) at ... #2 rocshmem::GDAContext::put (...) at ... #3 rocshmem::rocshmem_ctx_put (...) at ... <<< rocSHMEM API entry #4 my_kernel () at my_app.cpp:38 [rocSHMEM] Stuck in: rocshmem_ctx_put [HINT] Waiting for SQ spinlock held by another wavefront. The lock holder may itself be deadlocked in mlx5_poll_cq_until.

=== Summary === 1 wavefront(s) inside rocSHMEM 63 wavefront(s) outside rocSHMEM


### Reading the Output

**Groups** — Wavefronts with identical backtraces (after stripping runtime-variable
parts like addresses and argument values) are coalesced into a single group.
Each group shows which workgroups (WGs) and wavefronts (WFs) share that call
stack.  Template parameters and source file locations are kept in the key, so
`wait_until<int>` and `wait_until<long>` at different call sites produce
separate groups.

**`<<< rocSHMEM API entry`** — Marks the outermost rocSHMEM public API function
in the backtrace: the call the application made that led to the deadlock.

**`[rocSHMEM] Stuck in:`** — Names the matched API function for easy grepping.

**`[HINT]`** — A short diagnosis of the likely deadlock cause based on the
innermost recognized frame.

The innermost deadlock frame (the actual spinning loop) is printed in bold cyan.

**`=== Collective API Usage Issues ===`** — Appears after the per-group section
when incorrect `_wg` or `_wave` API usage is detected from the backtraces.
Three kinds of violations are reported:

| Issue | Description |
|---|---|
| `[BAD] … non-collective _wg call` | Wavefronts in the same workgroup are at different `_wg` API calls, or some are not in any `_wg` call. All wavefronts in a WG must call the same `_wg` primitive together. |
| `[BAD] … parameter mismatch in rocshmem_*_wg` | All wavefronts in a WG are at the same `_wg` call but with different argument values (e.g., different `pe` or `nelems`). |

`_wave` primitives are collective only within a single wavefront (all 64 lanes
must call with matching parameters). Different wavefronts — even in the same WG —
may independently call any `_wave` function with different parameters; that is valid.
Lane-level divergence within a wavefront is not visible from wavefront-level
backtraces and requires `--check-lanes` to detect.

#### Backend statistics (`--stats`)

When `--stats` is given, the script reads the rocSHMEM API call counters
directly from the stopped process's host memory without calling any function
in the inferior.  The counters live in `Backend::globalStats` (device-side,
written by GPU `atomicAdd`) and `Backend::globalHostStats` (host-side,
written by `std::atomic`).  Both arrays are in `hipHostMalloc`-pinned
memory, accessible to gdb without any cache flush.

Only non-zero counters are printed, grouped by device and host:

=== rocSHMEM Stats === PE 1 (counters reflect activity since process start) Device: put 8192 quiet 64 barrier_all_wg 4 Host: (all zero)


> **Requires `-DPROFILE=ON`** at CMake build time.  Without that flag
> `ROCStats` and `ROCHostStats` are empty `NullStats<N>` structs (no
> storage; all counters read as zero).  The section will note when this
> is the case.

The counters count operations since process start, not since the deadlock.
They show what the application was doing before it got stuck — e.g. a high
`barrier_all_wg` count with a non-zero `put` count suggests the collective
stalled after a put-heavy phase.

#### Lane-level parameter mismatch (`--check-lanes`)

When `--check-lanes` is added to the `rocshmem-deadlock-analyze` command, the
script iterates over the active lanes of every wavefront that is inside a `_wg`
or `_wave` collective call.  It switches rocgdb to each active lane, collects
the per-lane backtrace, and extracts the argument values for the collective
API frame.

If any two active lanes in the same wavefront disagree on a parameter, a
`LANE_PARAM_MISMATCH` issue is reported:

=== Collective API Usage Issues === [BAD] WG (0,0,0) WF 0 — lane parameter mismatch in rocshmem_putmem_wave (_wave contract) Differing parameter(s): pe Lane 0 [0,0,0]: pe=0 Lane 1 [1,0,0]: pe=1 Lane 2 [2,0,0]: pe=0 ...


This check covers both `_wave` (lanes diverging within one wavefront) and `_wg`
(lanes in a wavefront that should present unified parameters to the WG-wide
collective).

> **Performance note:** `--check-lanes` issues up to 64 additional `bt` calls per
> wavefront in a collective call.  On a busy kernel with many wavefronts this can
> be slow (several minutes).  Use it only when wavefront-level analysis has
> already identified a collective call of interest.

### Recognized Deadlock Patterns and Hints

#### mlx5 (Mellanox/ConnectX) backend

| Innermost frame | Hint |
|---|---|
| `mlx5_poll_cq_until` | Waiting for NIC completion (CQ polling). Check if NIC is responsive and if the remote PE is also stuck. |
| `acquire_lock` | Waiting for SQ spinlock held by another wavefront. The lock holder may itself be deadlocked in `mlx5_poll_cq_until`. |
| `mlx5_quiet` | Quiet operation waiting for all outstanding RMA ops to complete. Check NIC health. |

#### bnxt (Broadcom) backend

| Innermost frame | Hint |
|---|---|
| `bnxt_poll_cq_until` | Waiting for NIC completion (CQ polling). Check if the bnxt NIC is responsive and if the remote PE is also stuck. |
| `bnxt_post_wqe_rma` | Waiting for bnxt SQ spinlock held by another wavefront. The lock holder is likely itself deadlocked in `bnxt_poll_cq_until`. |
| `bnxt_quiet` | Quiet operation waiting for all outstanding RMA ops to complete (bnxt). Check NIC health. |

#### ionic (AMD/Pensando) backend

| Innermost frame | Hint |
|---|---|
| `ionic_quiet_internal_ccqe_single` | Waiting for NIC completion in CCQE mode (single-thread path). Check if the ionic NIC is responsive and if the remote PE is also stuck. |
| `ionic_quiet_internal_ccqe` | Waiting for NIC completion in CCQE mode. Check if the ionic NIC is responsive and if the remote PE is also stuck. |
| `ionic_quiet_internal` | Waiting for NIC completion (CQ polling). Check if the ionic NIC is responsive and if the remote PE is also stuck. |
| `spin_lock_acquire_unique` | Waiting for ionic SQ doorbell spinlock (exclusive) held by another wavefront. The lock holder may itself be stuck in `ionic_quiet_internal`. |
| `spin_lock_acquire_shared` | Waiting for ionic CQ spinlock (shared) held by another wavefront. The lock holder may itself be stuck in `ionic_quiet_internal`. |
| `ionic_quiet` | Quiet operation waiting for all outstanding RMA ops to complete (ionic). Check NIC health. |

#### Shared / IPC / user-level waits (all backends)

| Innermost frame | Hint |
|---|---|
| `wait_until_any` | Waiting for any element of a multi-element condition. Check if the remote PE is alive. |
| `wait_until_all` | Waiting for all elements of a multi-element condition. Check if the remote PE is alive. |
| `wait_until_some` | Waiting for some elements of a multi-element condition. Check if the remote PE is alive. |
| `wait_until` | Waiting for remote PE memory update (barrier/sync or user wait). Check if the remote PE is alive. |

---

## Color Support

Color is controlled by the `ROCSHMEM_DEADLOCK_COLOR` environment variable:

| Value | Behavior |
|---|---|
| `always` (default) | Always enable ANSI color codes |
| `auto` | Enable color when the output is a TTY; disable for files |
| `1`, `yes`, `true` | Always enable ANSI color codes |
| `0`, `no`, `false`, `never` | Always disable color codes |

Color can also be forced per-invocation with the `--color` / `--no-color`
flags of the interactive `rocshmem-deadlock-analyze` command.

The shell script's stdout summary is always plain text; ANSI codes are
stripped from the per-process output files before extracting the dominant hint.

### Color Scheme

| Element | Color |
|---|---|
| Section headers (`=== ... ===`) | Bold blue |
| Group headers (`--- Group N ---`) | Bold bright blue |
| Innermost deadlock frame | Bold cyan |
| `<<< rocSHMEM API entry` annotation | Bold green |
| `[rocSHMEM] Stuck in:` line | Bold bright red |
| `[HINT]` line | Bold cyan |
| Stuck-wavefront count in summary | Bold bright red |
| Not-stuck count in summary | Bold green |
| "No GPU threads found" notice | Red |

---

## Building with Debug Symbols

Full call-chain visibility requires debug symbols in the rocSHMEM library and
the application.  Use `RelWithDebInfo` for the best balance of performance and
debuggability:

```bash
# Configure the rocSHMEM build tree
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo <source_dir>
cmake --build . --target rocshmem --parallel 8

# Compile the application with matching flags
hipcc -O2 -g -fgpu-rdc -x hip my_app.cc \
      --offload-arch=<target> \
      -I<rocshmem_include> -I<mpi_include> \
      -c -o my_app.o

hipcc -fgpu-rdc --hip-link my_app.o \
      --offload-arch=<target> \
      <rocshmem_lib>/librocshmem.a <mpi_lib>/libmpi.so \
      -L/opt/rocm/lib -lamdhip64 -lhsa-runtime64 \
      -o my_app

Without debug symbols only the outermost non-inlined frame is visible. The <<< rocSHMEM API entry annotation still works (those functions are marked __attribute__((noinline))), but the [HINT] requires the inner frames (wait_until, mlx5_poll_cq_until, etc.) to be present.


Known Limitations

  • IPC backend: rocshmem_barrier_all_wg and related functions are fully inlined in the IPC backend even at RelWithDebInfo. The [rocSHMEM] Stuck in: line will not appear, but the [HINT] fires from the inner wait_until frame.

  • Release builds: Without debug symbols, internal frames (wait_until, mlx5_poll_cq_until, etc.) are invisible and no [HINT] is generated. The API entry frame is still detected when it is non-inlined.

  • Lane-level analysis performance: --check-lanes issues up to 64 bt calls per wavefront in a collective call. On large kernels this can take several minutes.

  • Single GPU per process: The script handles one GPU device per MPI rank. Multi-GPU-per-rank configurations are untested.

  • GPU thread format: Tested with rocgdb from ROCm 7.2. Earlier ROCm versions use a different thread name format (AMDGPU Thread X.Y (GPU, WG (...), WF (N))); the script includes a regex for this legacy format but it is untested.