Skip to content

[AMDGPU] Surface QuadrantsAssertionError after in-kernel assert (fix barrier hang) - #871

Open
paveltc wants to merge 4 commits into
Genesis-Embodied-AI:mainfrom
AMD-Ecosystem:fix/amdgpu-assert-trap-translate
Open

[AMDGPU] Surface QuadrantsAssertionError after in-kernel assert (fix barrier hang)#871
paveltc wants to merge 4 commits into
Genesis-Embodied-AI:mainfrom
AMD-Ecosystem:fix/amdgpu-assert-trap-translate

Conversation

@paveltc

@paveltc paveltc commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

On AMDGPU, a failed in-kernel assert previously emitted asm("S_ENDPGM"), which only terminates the faulting wavefront. Peer wavefronts still waiting on s_barrier then deadlock, and the host hangs forever in hipStreamSynchronize. This PR replaces that with a dispatch-wide __builtin_trap() and translates the resulting fault back into a proper QuadrantsAssertionError on the host, preserving the debug-mode assertion contract without hanging.

CUDA / CPU / Metal paths are unchanged. All new behavior is gated on debug + Arch::amdgpu.

Approach

__builtin_trap() faults the whole dispatch, so the host gets hipErrorLaunchFailure (719) rather than a hang — but the context is then dead, so the usual device-side error-retrieval kernels can no longer run. To preserve the error message:

  1. Pinned host-coherent mirror. In materialize_runtime (debug + amdgpu only) we hipHostMalloc(...Coherent) an AmdgpuAssertErrorState and publish its device-mapped address into the runtime. This mirrors the existing adstack_overflow_flag_dev_ptr precedent and survives a device fault.
  2. Device publish before trap. In quadrants_assert_format, the faulting wavefront (serialized under the existing error_message_lock) copies the message template + arguments into the pinned buffer, issues a system-scope fence (amdgpu_system_mem_fence, patched to an LLVM seq_cst fence in llvm_context.cpp), stores error_code last, then __builtin_trap()s.
  3. Host translation. AMDGPUFunction::operator() intercepts hipErrorLaunchFailure, and a debug-only hook reads the pinned state and raises QuadrantsAssertionError (a subclass of AssertionError). Subsequent 719s on the now-dead context are ignored so Program teardown does not terminate() from a destructor.

Testing

Validated on an AMD Instinct MI308X (gfx942), ROCm 7.2.4, base main:

  • tests/python/test_assert.py::test_amdgpu_assert_raises — a failed assert raises QuadrantsAssertionError with the formatted message; isinstance(e, AssertionError) holds.
  • tests/python/test_assert.py::test_amdgpu_assert_barrier_no_hang — one thread asserts while siblings hit block.sync(); raises instead of hanging (the original bug).
  • CPU sanity (test_assert_*) unchanged.

Both new tests run each case in an isolated child subprocess (the HIP context is dead after a trap; HIP is unsafe after fork) with a wall-clock timeout that fails on the barrier-hang regression.

CI notes

Upstream AMDGPU CI (test_gpu.ymltest_linux_amdgpu, runs-on: amdgpu) runs bare-metal on the self-hosted runner — no container — which matches the environment where the trap returns a catchable hipErrorLaunchFailure. Some ROCm/HSA configs (notably inside Docker) instead escalate the trap to an uncatchable SIGABRT; the tests treat a SIGABRT-killed child as pytest.skip (environment limitation) while still failing on timeout or on a wrong/absent exception, so no runner goes spuriously red.

Known limitations / possible follow-ups

  • After an assert, the HIP context is dead — one assert per process (tests isolate per subprocess). Accepted debug-mode limitation.
  • The pinned struct is duplicated as a layout-compatible host view (AmdgpuAssertErrorStateHostView) in llvm_runtime_executor.cpp; a static_assert on size/offsets would harden this.
  • The launch-failure hook and surfaced-flag are process-global singletons (assume a single active Program).

Made with Cursor

ptcherni and others added 3 commits August 14, 2026 14:57
Replace S_ENDPGM with __builtin_trap so peer wavefronts waiting on
s_barrier do not hang the host, and publish assert state into pinned
coherent host memory so the host can format QuadrantsAssertionError
after hipErrorLaunchFailure (HIP context is dead afterward).

Co-authored-by: Cursor <cursoragent@cursor.com>
Timeout is enforced by the subprocess.run(..., timeout=) path instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
Some ROCm/HSA configurations (notably inside Docker) turn the in-kernel
__builtin_trap() into an uncatchable SIGABRT rather than returning a
catchable hipErrorLaunchFailure, so the host never raises
QuadrantsAssertionError. Treat a SIGABRT-killed child as a skip (an
environment limitation) while still failing on the wall-clock timeout
(barrier-hang regression) and on a wrong/absent exception. Upstream
AMDGPU CI runs bare-metal, where the trap is catchable.

Co-authored-by: Cursor <cursoragent@cursor.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb26004957

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread quadrants/rhi/amdgpu/amdgpu_driver.h Outdated
Comment on lines +113 to +116
// After the assertion has been surfaced, further HIP calls on the dead context also return
// launch failure; ignore them so Program teardown does not terminate() from a destructor.
if (amdgpu_device_assert_already_surfaced()) {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Stop suppressing all post-assert launch failures

If an application catches the new QuadrantsAssertionError and performs another AMDGPU operation in the same process, the surfaced flag remains true and this global wrapper silently treats every subsequent hipErrorLaunchFailure as success. Because the HIP context is dead after the trap, later launches, copies, and synchronizations can therefore appear successful while returning stale or uninitialized results; restrict suppression to the known teardown path, or explicitly reject further executor use instead of changing every AMDGPU call globally.

AGENTS.md reference: AGENTS.md:L9-L13

Useful? React with 👍 / 👎.

…inding

Previously, once an in-kernel assert surfaced, AMDGPUFunction::operator() swallowed
every subsequent hipErrorLaunchFailure (719) as success until the next
materialize_runtime(). That masks dead-context errors if user code catches the
QuadrantsAssertionError and keeps issuing GPU work (Codex Genesis-Embodied-AI#871 P1).

Now 719 is suppressed only where throwing would std::terminate(): during teardown
(g_amdgpu_device_in_teardown, opened in LlvmProgramImpl::pre_finalize() before the
finalize() syncs, cleared on the next materialize) or while unwinding
(std::uncaught_exceptions() > 0). Any other post-assert GPU call now raises a clear
hard error instead of returning stale/uninitialized results.

Adds test_amdgpu_assert_dead_context_reuse_raises to lock in the behavior.
All three amdgpu assert tests pass on the MI308X.

Co-authored-by: Cursor <cursoragent@cursor.com>
@paveltc

paveltc commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the P1 (post-assert launch-failure swallowing) in 376f1ff

Thanks @codex — good catch. The prior code set a "surfaced" flag on the first in-kernel assert and then swallowed every subsequent hipErrorLaunchFailure (719) as success until the next materialize_runtime(). As you noted, that masks dead-context errors if user code catches the QuadrantsAssertionError and keeps issuing GPU work.

Fix: 719 is now suppressed only in the two situations where throwing would std::terminate():

  • Teardowng_amdgpu_device_in_teardown, opened in LlvmProgramImpl::pre_finalize() (before Program::finalize() runs its teardown synchronize() calls on the now-dead context) and cleared again on the next materialize_runtime().
  • Stack unwindingstd::uncaught_exceptions() > 0 (e.g. an RAII destructor firing a dead-context HIP call while the QuadrantsAssertionError itself propagates).

Any other post-assert GPU call now raises a clear hard error instead of returning stale/uninitialized results:

if (amdgpu_device_assert_already_surfaced()) {
  if (amdgpu_device_in_teardown() || std::uncaught_exceptions() > 0) {
    return;  // swallow: throwing here would std::terminate()
  }
  QD_ERROR(
      "AMDGPU device context is unusable after an in-kernel assertion failure; "
      "re-initialize Quadrants in a fresh process before issuing further GPU work "
      "(while calling {} ({}))",
      name_, symbol_name_);
}

Regression test: test_amdgpu_assert_dead_context_reuse_raises catches the first assertion, then reuses the context — and asserts that the reuse raises (not a silent success).

Validated on an MI308X (gfx942, ROCm 7.2.4): all three assert tests pass.

test_amdgpu_assert_raises                    PASS
test_amdgpu_assert_barrier_no_hang           PASS   (original barrier-hang scenario)
test_amdgpu_assert_dead_context_reuse_raises PASS   (reuse -> RuntimeError: "device context is unusable...")

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants