Repository navigation
fix(hrr): snapshot the pinned host memory kernels read, and restore it at replay - #13187
Conversation
✅ All Policy Checks Passed
📖 Need help? See the Policy FAQ for details on every check and how to fix failures. |
There was a problem hiding this comment.
🟡 Changes recommended
Snapshot tracking has unresolved fork deadlocks, failed-free state corruption, and a pinned-pointer replay edge case.
3 open findings
What changed in this PR
Adds capture and replay support for pinned host-memory snapshots used by kernels.
Changes:
- Tracks pinned allocations and snapshots changed contents before launches.
- Validates and restores snapshots during replay.
- Adds format v8 documentation, configuration, and tests.
| File | Description |
|---|---|
projects/hrr/tools/gen_hrr_api_args.py |
Bumps generated format to v8. |
projects/hrr/include/hrr/hrr_api_args.h |
Updates generated format definition. |
projects/clr/rocclr/utils/flags.hpp |
Adds snapshot configuration flags. |
projects/clr/hipamd/src/hrr/hip_capture.cpp |
Implements allocation tracking and snapshot capture. |
projects/hrr/playback/hip_playback.h |
Adds snapshot validation and counters. |
projects/hrr/playback/hip_playback.cpp |
Restores snapshots before launches. |
projects/hrr/playback/hrr_reader.h |
Documents snapshot records. |
projects/hrr/playback/hrr_reader.cpp |
Parses snapshot records. |
projects/hrr/playback/hrr_playback.cpp |
Reports restoration statistics. |
projects/hrr/tests/unit/hrr_format_test.cc |
Tests record parsing and validation. |
projects/hrr/tests/integration/hrr_pinned_host_test.cc |
Adds pinned-memory round-trip tests. |
projects/hrr/tests/integration/hrr_roundtrip_test.cc |
Disables snapshots for divergence-specific tests. |
projects/hrr/tests/integration/CMakeLists.txt |
Registers the new integration tests. |
projects/hrr/tests/hrr_test_common.hh |
Supports capture environment overrides. |
projects/hrr/README.md |
Documents user-facing settings. |
projects/hrr/DESIGN.md |
Documents v8 design, limits, and threat model. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…e low pinned addresses The pinned tracking mutex joins the writer's fork protocol, taken first in prepare. A release call keeps the allocation tracked unless it succeeds, with snapshots held off for its duration. Replay translates a struct word capture flagged inside a pinned host allocation even when its low 32 bits fail the device-address shape check.
…k and failed-free handling Replay restores no pinned snapshot while a graph capture is open, but capture still made such a record the allocation's last, so the next direct launch on the same bytes got no record and replay ran it on stale memory. A record written while any stream in the process captures no longer becomes the last. The pinned workload now launches into a discarded graph capture before its third launch, and the roundtrip checks that launch is still restored. The pinned mutex no longer joins the fork handlers, and a free or unregister stops tracking the allocation before the call again, whatever it returns. DESIGN.md lists a fork during a snapshot as a known limit again.
|
🤖 AI-assisted pre-review (for @mangupta) Re-reviewed the updated head ( |
|
🤖 AI-assisted pre-review (for @mangupta) Re-reviewed the updated head ( |
danielbolsson
left a comment
There was a problem hiding this comment.
The design is sound, and the replay-side record validation is solid. Two things to address before merge, plus a few minor ones inline.
- Stale thread replies. The replies on r4246061970 (fork) and r4246062066 (failed free) still say "Fixed in b3bd5da", but fa1ffc5 reverted both fixes. Please update them so nobody resolves the threads on that basis. Inline at
g_pinned_mu: why was the fork handler dropped? - PR description. It says
HRR_VERSIONgoes to 8; the code bumps it to 9. Were the test results re-run after fa1ffc5 and the develop merge?
…ross fork A pinned snapshot hashed the application's memory and then read it again to write the blob, so a host thread or queued GPU work could change the bytes in between. The blob was then filed under a hash that did not name its contents, and the blob store's dedup served those bytes for any later blob with that hash. The snapshot now copies each allocation into a buffer under the tracking lock, releases the lock, and hashes and writes the copy. The buffer is allocated per snapshot and freed once written, before the launch; a failed allocation is listed under unreplayable_apis like an oversized one. The tracking lock is still held while a launch writes its event, so a fork() from another thread can still leave a child with it locked. Its fork handler is back: fa1ffc5 dropped it only to keep this change small, not over lock order. It is registered after the writer's handlers, so fork takes the pinned lock first, then the writer's, the order a launch takes them in. Unit_HRR_PinnedHostFork forks while another thread snapshots a 32 MiB allocation; each child takes the lock through a failing hipFree, and one that does not exit within 10 s fails the case.
Replay skipped every pinned restore while any graph capture was open, and capture kept every record made meanwhile from becoming the allocation's last. A direct launch on a stream that was not capturing lost its restore. Both sides now look at the launch stream only: replay asks hipStreamIsCapturing, capture reads the stream's capture status. The pinned workload launches on a second stream during its graph capture; replay must restore that launch, and the next launch then has nothing new to record. Unit_HRR_PinnedHostStructLowBits_Direct maps its fixed address with MAP_FIXED_NOREPLACE and skips when the range is taken, or when an older kernel treats the address as a hint and maps elsewhere. It runs last in Unit_HRR_PinnedHostRoundtrip, since a skip ends that case.
…raghetti/hrr-pinned-host-min
Replay synchronizes the launch stream before it writes a pinned snapshot back. When another stream was capturing in global mode, that sync invalidated the capture, and its hipStreamEndCapture failed with 901. The pinned workload hits this: it launches on a second stream while its graph capture is open, and Unit_HRR_PinnedHostRoundtrip failed on MI350. The launch stream is not capturing, so waiting for it is safe. Replay now switches the thread to relaxed capture mode for that one sync and puts the old mode back.
HIP_HRR_HOST_SNAPSHOTS and HIP_HRR_HOST_SNAPSHOT_MAX_MB were release flags in rocclr/utils/flags.hpp. They only tune capture, so they now live in hip_capture.cpp, next to the code that uses them, and flags.hpp is back to develop's. The names, defaults and parsing are unchanged: the snapshot switch is on unless set to something other than "true" or a non-zero number, and the cap is 64 MiB unless set. Each is read once, since every launch asks. Unit_HRR_PinnedHostSnapshotCap captures a 2 MiB pinned allocation with the cap at 2 and at 1 MiB. Within the cap, replay restores it and matches. Over it, the manifest lists it as unreplayable, replay restores nothing and the device-to-host checks fail. The case set in integration.yaml gains it and Unit_HRR_PinnedHostLarge_Direct.
develop took HRR_VERSION 9 for hipKernelSetAttributeForDevice (1baee77), so the pinned host snapshot records move to v10. The version notes in hrr_api_args.h, gen_hrr_api_args.py and DESIGN.md keep both entries.
|
HRR_VERSION is now 10, since develop took 9 in 1baee776d0. The description and the MI350X results are updated for 5643bd7542. |
|
🤖 AI-assisted pre-review (for @mangupta) Re-reviewed the updated head ( |
|
🤖 AI-assisted pre-review (for @mangupta) Re-reviewed the updated head ( |
There was a problem hiding this comment.
🟡 Changes recommended
Snapshot ordering, allocation lifetime, blocking synchronization and error propagation issues can cause incorrect replay, use-after-free or hangs.
6 open issues
Avoid blocking replay submission during stream restoration · New Protect allocation lifetime during concurrent replay restoration · New Abort kernel launch after fatal snapshot restoration failure · New Deduplication races with event write ordering · New Normalize per-thread default streams before capture detection · New Propagate synchronization errors and abort failed restoration · New
3 resolved since last review
Review effort: Balanced
…ng for it Replay copied a launch's pinned snapshots in after hipStreamSynchronize, which hangs when the stream waits on work the replay thread submits later, and left the copy to race a free on another replay thread. The copy is now a host function queued ahead of the kernel; hipHostFree and hipHostUnregister wait for every stream. A blob the host memory cap refuses or a failed enqueue ends the launch before the kernel runs. Since a restore is ordered on its own stream only, capture records again for a launch on another stream than the allocation's last record.
|
🤖 AI-assisted pre-review (for @mangupta) Re-reviewed the updated head ( |
danielbolsson
left a comment
There was a problem hiding this comment.
Re-checked d6b99b1 against my earlier comments: blobs are now hashed from a copy taken under g_pinned_mu, the atfork handler is back in pinned-then-writer order, capture and replay both use the launch stream's capture state, the low-bits test uses MAP_FIXED_NOREPLACE and skips on EEXIST, and the restore is now a host function queued ahead of the kernel instead of a stream sync. The two stale thread replies are updated, and the develop-merge resolutions keep v10 consistent.
Non-blocking nits:
projects/hrr/playback/hip_playback.cpp:490,projects/hrr/DESIGN.md:559-ihipFree/ihipHostUnregistercallSyncAllStreams()only on the allocation's device, so a restore queued on another GPU's stream is not waited for. Say "every stream of the allocating device", or sync the launch device too.projects/hrr/tests/integration/hrr_pinned_host_test.cc:363-Unit_HRR_PinnedHostForkpasses if capture is silently off. Also require a blob of at least 32 MiB in the archive.projects/clr/hipamd/src/hrr/hip_capture.cpp:300- no test races a host writer against the snapshot, so reverting to hashing the live bytes stays green. Add a test only if it can be made deterministic.
…ees and the extra[] launch hipMallocHost was replayed into a pinned buffer but never tracked, so a kernel reading it ran on replay's empty buffer. Test that hipFree, hipFreeHost and hipHostUnregister stop the tracking, and a hipModuleLaunchKernel launch whose arguments come in extra[]. DESIGN.md now says what the enqueue-time copy does to queued device writes into the allocation.
|
🤖 AI-assisted pre-review (for @mangupta) Re-reviewed the updated head ( |
danielbolsson
left a comment
There was a problem hiding this comment.
Re-checked a7b0018 on top of my approval of d6b99b1: the new pinned_hipMallocHost wraps the generated capture_hipMallocHost shim the same way as pinned_hipHostMalloc (tracks only on success, untracked by the existing hipFreeHost/hipFree wrappers), and the DESIGN.md text on queued device writes matches the enqueue-time copy and the threads you answered. The new tests cover hipMallocHost, all four release paths and the extra[] launch; not built or run on GPU here, and only therock-pr-bot fails in CI.
Nit: projects/hrr/DESIGN.md:547 - the edited line is over the file's wrap width; re-flow the paragraph.
…hrr-metadata-only-capture #13187 took archive version 10 for pinned host snapshot records, so the metadata-only flag moves to v11: HRR_VERSION, the generator template, the DESIGN.md history and current-version lines, and the CHANGELOG entry. hrr_capture_direct keeps HIP_HRR_CAPTURE_CONTENT=1 and applies the caller's environment after it. Unit_HRR_ForkDuringBlobWrite (#13696) needs a blob write to hold, and Unit_HRR_PinnedHostFork (#13187) recorded with content on develop, so both now set HIP_HRR_CAPTURE_CONTENT=1.
#13187 writes each hipHostMalloc, hipMallocHost or hipHostRegister allocation a kernel launch points into to blobs/ before the launch. The inventory gets a row for it, in contents mode only since the snapshot goes through write_blob, and the strings list names it. test_privacy_doc.py checks that PinnedSnapshot writes a blob and that the row names the three APIs.
TheRock Submodule Bump ActivityNewest first
|


Motivation
A kernel can read pinned host memory directly, through
hipHostMallocorhipHostRegister. Capture did not record what that memory held, so replay ran those kernels on whatever its own fresh buffer held. The ROCM-31827 vLLM archive has 147 such launches.This change records, before each launch, the pinned host memory the kernel can read, and replay puts those bytes back before the kernel runs.
Technical Details
hipHostMalloc,hipMallocHostandhipHostRegister, and drops them on free and unregister.HRR_VERSIONgoes to 10 (developtook 9 forhipKernelSetAttributeForDevice). Older archives are refused, as with the previous bump.hipHostFreeandhipHostUnregisterwait for every stream, so the allocation outlives the copy. A blob refused by the host memory cap, or a failed enqueue, ends the launch before the kernel runs. Restores are skipped for a launch into a capturing stream, and capture checks the same launch stream. The summary printsPinned host snapshots: N restored, M refused, K skipped (graph capture).HIP_HRR_HOST_SNAPSHOTS=0turns snapshots off.HIP_HRR_HOST_SNAPSHOT_MAX_MB(default 64) caps the size of one allocation. Both only tune capture, sohip_capture.cppreads them itself, with the same parsing as the CLR flags. The change stays inside the HRR directories.DESIGN.mddescribes the records, their limits and a short threat model for replaying untrusted archives (for AIRUNTIME-2853).Issue Tracking
JIRA ID: AIRUNTIME-2894
Defect: ROCM-31827
Test Plan
Unit_HRR_PinnedHostRoundtrip: a kernel reads a pinned buffer the host changes between launches, throughhipHostMalloc,hipMallocHost,hipHostRegister, a struct passed by value and ahipModuleLaunchKernelwithextra[]. One launch goes into a graph capture and one onto a second stream while the capture is open. Replay must match the recording byte for byte. With snapshots off, it must not.Unit_HRR_PinnedHostSnapshotCap: a 2 MiB allocation with the cap at 2 MiB replays correctly. With the cap at 1 MiB it is listed underunreplayable_apis, nothing is restored and replay does not match.Unit_HRR_PinnedHostStreamsRoundtrip: the pinned-input kernel on one stream queues behind a kernel waiting for a flag that a later kernel on a second stream sets, after a launch there reads the same input. Replay must not hang and must match byte for byte.Unit_HRR_PinnedHostBlobRefused: a snapshot blob over--max-file-bytesends the replay before its kernel launches.Unit_HRR_PinnedHostFork: forks while another thread snapshots a 32 MiB allocation; every child must take the tracking lock and exit.Unit_HRR_PinnedHostFreedRoundtrip: afterhipHostFree,hipFree,hipFreeHostorhipHostUnregister, capture no longer snapshots the address.Unit_HRR_Format_HostSnapshotRecord: the record round trip, and records refused for bad bounds and short payloads.Unit_HRR_DivergenceAbortRoundtripandUnit_HRR_NullOptionalPtrRoundtripnow capture with snapshots off, since their divergence comes from a pinned flag.amdhip64with-DHIP_HRR_CAPTURE_ENABLED,hrr-playbackand the tests for gfx950, and ran every Catch2 case, hidden ones included, on an MI350X.hipMallocHosttracking with untrack on the other frees and theextra[]launch, payload bounds check, fork handler, launch-stream capture state, the blocking restore, per-content-only records), and a build whose cap ignoresHIP_HRR_HOST_SNAPSHOT_MAX_MB.Test Result
Format_HostSnapshotRecordfailsdevelopat 362ea65125. The ones that fail there fail here too:Unit_HRR_FirstMallocRoundtrip, and theDiskSpace,DispatchSwap, fork and crash_Directworkloads run without their visible testPinnedHostRoundtripfails; the cap control fails onlyPinnedHostSnapshotCap; each single-fix control fails the test written for itPinnedHostRoundtripfails on every family, andFormat_HostSnapshotRecordon both unit jobsSubmission Checklist