Skip to content

fix(hrr): make decode-and-triage produce a correct finding on real archives - #9758

Open
andraghetti wants to merge 5 commits into
developfrom
users/andraghetti/hrr-skill-consolidation
Open

fix(hrr): make decode-and-triage produce a correct finding on real archives#9758
andraghetti wants to merge 5 commits into
developfrom
users/andraghetti/hrr-skill-consolidation

Conversation

@andraghetti

@andraghetti andraghetti commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

The decode-and-triage skill was run end to end against real archives on a gfx950 host, and reviewed against the HRR runbooks. It did not survive either exercise: on a crashing archive it produced no finding at all, and on the archives where it did produce one, the verdict was often wrong.

This is a skill-only change. No capture or playback source is touched.

What was wrong

# Symptom on a real archive Cause
1 Native replay impossible even with hrr-playback supplied export_playback_env ended in a trailing &&, which under set -e became the function's exit status and killed the script before it printed the path
2 A replay that faulted produced a log and no finding run_native_replay re-armed set -e inside itself, cancelling the set +e the caller wraps it in, so any non-zero replay exit ended the script before the analyzer ran
3 A hung replay produced nothing and never returned no bound on the replay, so the analyzer was never reached
4 Metadata-only triage reported no events, kernels or completeness --no-replay never resolved a playback binary, so --info never ran
5 A packaged playback failed to load its own libamdhip64 the library path handled only a playback inside a CLR build tree, not bin/ and lib/ siblings
6 A queue abort reported as a hang with an unknown kernel hang was reachable from any HSA_STATUS_ERROR_*, and the abort line carries a kernel but no faulting address, so the memory-fault regex could not see it
7 A read-only page fault reported as replay_fatal_api the generic abort branch was tested before the memory-fault branch
8 A clean replay reported a hipBLASLt GEMM as its kernel the name was matched out of the archive's own kernel listing
9 A fault with no attribution reported no kernel at all the --info kernel table was not parsed, so a single-kernel archive named nothing
10 The kernel name dropped on some ROCm builds RE_MEM_FAULT_ERR required a host: field that not every build emits
11 Replay picked the busiest GPU and reported its full VRAM as free the awk matched Used Memory and Total Memory as separate patterns, but the used line reads VRAM Total Used Memory and matches both
12 Replay could land on a device other than the one picked the native path masked with ROCR_VISIBLE_DEVICES, which re-indexes underneath a HIP mask; the Windows and container paths already used HIP_VISIBLE_DEVICES
13 Complete:, Events:, Kernels: and both format versions unreported uppercase-only matcher, unparsed labelled lines, versions recorded only in prose
14 Last progress kernel always empty assigned unconditionally, so the --info pass erased what the replay log had recorded

Two of these deserve a note.

Nothing beats a wrong verdict for cost. Defects 6 and 8 are the expensive ones: one reports a hang that never happened and drops the kernel the runtime had just named, the other prints a GEMM next to a PASS, where it reads as a culprit. Both send a kernel developer after a defect the run never observed.

A hang is now bounded, not detected. The replay is stopped after HRR_REPLAY_TIMEOUT seconds, default 1800, and the runner records that in the log so the finding says hang rather than nothing. The bound goes around the playback process, not around a container that may wrap it: a timeout on the container leaves the replay running inside it.

What changed in behaviour

  • Replay runs with --sync-after-launch by default; --no-sync opts out. Measured cost on a real vLLM capture of 13233 kernels: 41.7 s against 39.6 s.
  • A clean replay implicates no kernel. A fault with no attribution in the log takes the kernel from an archive holding exactly one, marked in the finding as inferred; more than one stays unknown, and names truncated by the --info column width are never used.
  • New outcome HANG. HSA_STATUS_ERROR_MEMORY_FAULT classifies as illegal_memory_access, the other HSA statuses as replay_aborted, and the raw status goes in the notes.
  • A fault on a <<<>>>-launched ATen kernel keeps its class and gains a caveat. Capture records the pointers these kernels pass inside by-value structs and replay translates them, so such a fault can be genuine and reclassifying it would suppress a real finding; but the translation is a heuristic and older archives carry no offsets, so the skill asks for the original failure signature instead of resolving the ambiguity.

Testing

Fixtures are unedited tool output recorded on a gfx950 host, so a change in what hrr-playback prints fails a test rather than silently degrading a finding. Device selection, playback discovery and the library layout are covered without a GPU by lifting the shell functions out of the script and running them against recorded input.

Unit suite: 24 tests before, 42 after. One test in test_check_replay_compat.py fails identically on unmodified develop on both Linux and macOS and is untouched here. One test skips below bash 4.4.

End to end with triage_archive.sh on a gfx950 host:

Archive Result
vLLM inference capture, format v4 13233 kernels replayed, 32 device-to-host checks exact, PASS / replay_pass, no kernel implicated, Complete: yes, 185469 events
Capture whose workload faults fault reproduced, run survives it, MAF / illegal_memory_access with fault address, kernel crash_oob and the dispatch grid. Previously: no finding at all
PyTorch capture indexing past its embedding table replay reproduces the capture-time failure exactly, same HSA_STATUS_ERROR_EXCEPTION on the same kernel. ABORT / replay_aborted, full indexSelectSmallIndex signature, ATen caveat. Previously: UNKNOWN / hang, kernel unknown
Hanging capture, 45 s bound HANG / hang with a finding. Previously: ran until killed, no finding
Archive the playback build cannot read version_mismatch with archive version 3 and reader version 4, no kernel invented
Metadata-only, no GPU 5 events, 1 kernel, crash_oob, Complete: no. Previously: every field n/a

Known gaps

  • Multi-process archives are untested: none of the 13 archives available here has more than one pid-* directory, and load_capture_metadata reads only <archive>/manifest.json, so an archive root loses the preflight and the capture fields.
  • A hang is inferred from the bound firing, not from any signal the replay emits; a replay legitimately slower than the timeout will be reported as a hang.
  • Windows was not exercised. triage_archive.ps1 gains no changes here beyond already using the HIP mask.

Issue Tracking

JIRA ID: AIRUNTIME-2597

Submission Checklist

Four defects in the decode-and-triage skill, each reproduced against replay
output recorded on a gfx950 host, and each covered by a test built from that
output rather than from a hand-written sample.

A GPU memory fault was reported as a plain API error. Surfacing a fault
through --sync-after-event makes hrr-playback print "Fatal: GPU error after
..." for the same fault the runtime reports, and the abort branch was tested
before the memory-fault branch, so a read-only page fault classified as
replay_fatal_api. That is the mode used to localize a fault, so the flagship
fault class was the one being lost. Memory faults are now classified first.

A clean replay was attributed a kernel. The archive lists the kernels it ran,
a GEMM was matched out of that listing, and the finding printed it next to a
PASS as though it were a culprit. Observed on the replay of a real vLLM
inference capture that passed every device-to-host check. A clean replay now
implicates nothing.

The opposite case was silent: a memory fault can tear the process down before
the failing dispatch is attributed, leaving a log with no kernel at all. The
--info kernel table is now parsed, and when the archive holds exactly one
kernel the finding names it and records that the name was inferred. More than
one stays unknown. Names truncated by the table's column width are ignored,
since a cut-off symbol cannot be looked up; in the vLLM capture every name in
the table is truncated.

Device selection picked the busiest card. The awk matched "Used Memory" and
"Total Memory" as separate patterns although the used line reads "VRAM Total
Used Memory" and so matches both. On two devices reporting the same total,
where the first is nearly full, it selected the first and reported its whole
VRAM as free. On a shared host that sends the replay onto somebody else's
device. Totals are now collected per device and subtracted once.

Also mask the replay with HIP_VISIBLE_DEVICES. hrr-playback is a HIP program,
and setting ROCR_VISIBLE_DEVICES re-indexes devices underneath a HIP mask,
which can place the replay on a device other than the one picked. The Windows
and container paths already used the HIP mask, so this makes the three agree.

Smaller parsing gaps closed at the same time: the Complete: matcher accepted
only uppercase while --info prints "yes (clean shutdown)", so a clean archive
never recorded its state; the labelled Events:, Kernels: and Recovered: lines
were unparsed, so an --info run carried no totals; and a version mismatch
recorded a note but not the two versions it found.

JIRA ID: AIRUNTIME-2597
@andraghetti
andraghetti requested review from a team as code owners August 6, 2026 08:59
Copilot AI lite review requested due to automatic review settings August 6, 2026 08:59
@therock-pr-bot

therock-pr-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

✅ All Policy Checks Passed

Check Status Details
📝 PR Description ✅ Pass
Forbidden Files ✅ Pass
🧪 Unit Test ✅ Pass
🚫 Draft PR 🔜 To Be Enabled
🚩 Feature Flag 🔜 To Be Enabled
📊 Code Coverage 🔜 To Be Enabled

🎉 All policy checks passed!

📖 Need help? See the Policy FAQ for details on every check and how to fix failures.

🙋 Wish to Override Policy?

Copilot AI 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.

Pull request overview

This PR fixes multiple correctness issues in the decode-and-triage skill’s replay analysis and replay execution wrapper so that reported fault classes, kernel attribution, archive metadata, and GPU selection reflect what the run actually supports.

Changes:

  • Fix fault classification priority so GPU memory faults aren’t masked by the generic “Fatal … aborting” line.
  • Rework kernel attribution so clean replays report no kernel, and single-kernel archives can infer attribution only when unambiguous (ignoring truncated --info names).
  • Fix GPU picking and native replay masking: correct rocm-smi parsing for free VRAM and switch native replay masking to HIP_VISIBLE_DEVICES.

Reviewed changes

Copilot reviewed 8 out of 11 changed files in this pull request and generated no comments.

Show a summary per file
File Description
projects/clr/hipamd/src/hrr/skills/decode-and-triage/SKILL.md Documents kernel attribution rules (no kernel on PASS; inferred-only when unambiguous).
projects/clr/hipamd/src/hrr/skills/decode-and-triage/scripts/triage_archive.sh Fixes pick_gpu VRAM parsing and masks native replay with HIP_VISIBLE_DEVICES.
projects/clr/hipamd/src/hrr/skills/decode-and-triage/scripts/test_triage_archive.py Adds unit tests for GPU selection parsing and masking behavior (no GPU required).
projects/clr/hipamd/src/hrr/skills/decode-and-triage/scripts/test_analyze_replay_finding.py Extends tests for fault classification precedence and end-to-end recorded fixtures.
projects/clr/hipamd/src/hrr/skills/decode-and-triage/scripts/analyze_replay_finding.py Implements classification fix, parses --info totals and kernel table, records version fields, and finalizes attribution once all inputs are parsed.
projects/clr/hipamd/src/hrr/skills/decode-and-triage/scripts/fixtures/version_mismatch.log Adds recorded fixture for version-mismatch parsing.
projects/clr/hipamd/src/hrr/skills/decode-and-triage/scripts/fixtures/rocm_smi_vram.txt Adds recorded rocm-smi output fixture for GPU-selection tests.
projects/clr/hipamd/src/hrr/skills/decode-and-triage/scripts/fixtures/replay_pass.log Adds recorded passing replay fixture.
projects/clr/hipamd/src/hrr/skills/decode-and-triage/scripts/fixtures/replay_memory_fault.log Adds recorded memory-fault replay fixture.
projects/clr/hipamd/src/hrr/skills/decode-and-triage/scripts/fixtures/info_pass.txt Adds recorded --info fixture for clean archive totals and truncated kernel name handling.
projects/clr/hipamd/src/hrr/skills/decode-and-triage/scripts/fixtures/info_crash.txt Adds recorded --info fixture for single-kernel crash archive attribution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Running the skill end to end on a gfx950 host showed native replay was
impossible even with hrr-playback present and supplied through HRR_PLAYBACK:
triage_archive.sh reported "ensure_playback.sh --build failed" and stopped.

export_playback_env ended with `[[ -n "$rocr_lib" ]] && export ROCR_LIB=...`.
Under `set -e` that trailing conditional is the function's exit status, so
whenever the ROCR lib dir does not resolve, which is any host that is not a
CLR source tree, the function returned 1 and took the script down with it
before it printed the playback path. The binary was found and then discarded.

Make the export a plain if, and cover it with a test that runs the script
outside any CLR tree with HRR_PLAYBACK pointing at a stub. The test needs
bash 4.4 or newer, since expanding an empty array under `set -u` is an error
on the bash 3.2 that macOS still ships, and these scripts target Linux.

JIRA ID: AIRUNTIME-2597
Running the skill against a faulting archive on a gfx950 host showed it
produced no finding at all in the one case it exists for.

run_native_replay re-enabled `set -e` inside itself before `return "$rc"`,
cancelling the `set +e` the caller wraps it in precisely so a failing replay is
survivable. Any non-zero replay exit therefore killed the script before the
analyzer ran, so a replay that faulted left a log and nothing else. The inner
`set -e` is dropped; the caller restores it as it always intended to.

Replay now runs with --sync-after-launch by default, with --no-sync to opt out.
Default playback serializes the GPU once at the end, so a fault is reported but
never attributed and the finding carries no failing event and no kernel. On the
faulting archive the synchronized run additionally makes the runtime print the
dispatch packet and name the kernel outright. Measured on a real vLLM inference
capture of 13233 kernels, the option costs 41.7 s against 39.6 s, so the
attribution is close to free.

Three parser gaps found in review against the HRR runbooks:

- The Memory Fault Error bracket varies by ROCm build. Requiring the `host:`
  field dropped the kernel name on every build that starts the bracket at
  `GPU index:`, which is the one field the report exists to give. The regex now
  anchors on the two fields actually consumed. Both forms occur on our own
  hardware, so neither can be assumed.
- The `--info` kernel rows tolerate a missing id column, and the kernel total is
  recovered from the API call-count block when no `Kernels:` line is printed.
  Without the latter the total stays unknown and the single-kernel inference can
  never fire on a crash-resilience archive.
- Per-event progress lines are parsed, so the last launch to start before a
  fault stands in when the runtime's fault line named no kernel, recorded in the
  finding as such.

A fault on a <<<>>>-launched ATen kernel keeps its class rather than becoming a
recording artefact. Capture records the pointers these kernels pass inside
by-value structs and replay translates them, so such a fault can be genuine and
reclassifying it would suppress a real finding. The translation is a value-based
heuristic and archives predating it carry no recorded offsets, so the finding
carries a note and the skill asks for the original failure signature instead of
resolving the ambiguity either way.

JIRA ID: AIRUNTIME-2597
Recorded a PyTorch capture whose embedding lookup indexes past its table, and
replayed it on a gfx950 host. The replay reproduced the fault and the runtime
named the culprit outright:

  Callback: Queue ... aborting with error : HSA_STATUS_ERROR_EXCEPTION: An
  HSAIL operation resulted in a hardware exception. code: 0x1016
  [host: ..., GPU index: 0, kernel: void at::native::(anonymous
  namespace)::indexSelectSmallIndex<...>]

The finding called it a hang with an unknown kernel. Two reasons, both fixed.

That bracket carries a kernel but no faulting address, so the memory-fault
regex could not see it and the one name the runtime handed us was dropped. It
is now read from the abort line when nothing else named a kernel.

`hang` was reachable from any HSA_STATUS_ERROR_{MEMORY_FAULT,ABORTED,EXCEPTION}.
None of those is a hang: MEMORY_FAULT says it is a fault, and EXCEPTION is a
hardware exception, which an out-of-bounds access raises. MEMORY_FAULT now
classifies as illegal_memory_access and the other two as replay_aborted, the
outcome follows the same split instead of staying UNKNOWN next to a decided
fault class, and the raw HSA status is recorded in the notes. A genuine hang
shows up as no progress against the clock, which needs a replay timeout the
skill does not have; reporting one from an HSA status alone was sending the
reader after stalled work that never existed.

The kernel family now recognises the demangled `at::native::` form as well as
the mangled `_ZN` one, since the memory-fault line prints one and the queue
abort line the other. That also lets the ATen caveat fire, which this capture
is the first real case of.

The replay log is checked in as a fixture, unedited.

JIRA ID: AIRUNTIME-2597
Four defects found by exercising the paths the earlier commits did not: a hang,
metadata-only triage, and a packaged playback layout.

A hung replay never returned, so the analyzer never ran and the archive yielded
no finding whatsoever, which is the same silence as a crash used to produce.
The replay is now bounded, default 1800s, `HRR_REPLAY_TIMEOUT=0` disables. The
bound goes around the playback process rather than around any container that
may wrap it: a timeout on the container leaves the replay running inside it.
When the bound fires the runner records it in the log, the analyzer classifies
`hang` from that marker, and the outcome is `HANG`. Verified with a 45s bound
against a hanging archive: a finding is produced where none was before.

Metadata-only triage silently discarded every piece of archive evidence.
`--no-replay` never resolved a playback binary, so `hrr-playback --info` was
never run and events, kernel totals, kernel names and the completeness flag
were all absent from the finding. That is the one mode meant to work without a
GPU. It now locates a binary without ever building one and, when there is none,
degrades exactly as before.

That fix alone was not enough, which exposed the third defect: the library path
only handled a playback inside a CLR build tree. A packaged playback ships as
bin/ and lib/ siblings, and its own lib dir was never added, so the binary
loaded the system libamdhip64 and failed on the symbols it was built against.
This took out the metadata-only path and would take out replay on any installed
playback.

`last_progress_kernel` was assigned unconditionally after its match loop, so
the `--info` pass, which has no progress lines, erased the kernel the replay log
had already recorded. The default pipeline always parses both, so the field was
always empty in practice.

Two corrections to what the earlier commits claimed. The per-event lines the
finding falls back on come from `--verbose`, not from the sync flags:
`hrr_playback.cpp` gates them on `ctx.verbose` alone, so the note now says so.
And the GPU is no longer picked in metadata-only mode, which announced a device
it never used.

JIRA ID: AIRUNTIME-2597
@andraghetti andraghetti changed the title fix(hrr): correct triage fault class, kernel attribution and GPU pick fix(hrr): make decode-and-triage produce a correct finding on real archives Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants