Kernel Replay — Memory Snapshot and Restore#
Replaying a kernel is only meaningful if every pass sees the same inputs. Kernel replay achieves that by capturing the replaying agent’s device memory before the first pass and writing it back between passes. This page describes what is captured, what is deliberately excluded, what happens when capture fails, how this differs from the earlier hashed snapshot, and the limitations that follow from the capture mechanism.
Implementation lives in source/lib/rocprofiler-sdk/kernel_replay/memory_snapshot.cpp (capture and
restore) and memory_tracker.cpp plus utils.cpp (the allocation inventory).
Snapshot mechanism: this version vs hashing#
This version is a plain in-memory full copy. snap(agent) copies every tracked device allocation
owned by agent into host RAM; restore() copies the whole region back. Every region, every pass.
There is no hashing, no dirty-page diffing, no chunking, and no disk backing.
This version |
Expected later |
|
|---|---|---|
Backing store |
Host buffer per region |
Open; hashing aims to cut bytes moved |
Copy shape |
One |
Restore only dirty regions |
Dirty detection |
None — every byte of every region is written back |
Host-side hashing (after a device-to-host read) and/or device-side hashing (hash on the GPU, restore only dirty pages) |
Host RAM |
The entire tracked footprint is duplicated for the replay |
Reduced if hashing restores only dirty bytes |
Dirty-page hashing is not implemented, and snapshots are never spilled to disk. Every tracked region is copied in full, every time. Hashing is expected in a future version, on the host and/or the device, because snapshot and restore are bandwidth-bound and scale linearly with the tracked footprint.
Allocation tracking#
The tracker maintains an inventory of live device allocations. It is populated by wrappers installed over four HSA entry points:
Wrapped function |
Table |
|---|---|
|
AMD extension |
|
AMD extension |
|
core |
|
core |
Wrappers are installed only for the first library instance; a later instance would capture the
already-installed wrapper as its own chained next_ pointer and recurse. Recording is gated on a
relaxed atomic flag that is set when a tool configures the kernel replay callback tracing service, so
a run that never uses replay pays only that load plus the chained call.
The inventory itself lives behind a common::Synchronized wrapper, so every access goes through
rlock/wlock rather than a bare mutex.
Teardown guard#
The installed wrappers stay live for the whole process, which means HIP’s own __cxa_finalize can
call the free wrapper after rocprofiler has destroyed the inventory’s static object.
record_alloc(), record_free() and snap_inventory() therefore early-out on
registration::get_fini_status() > 0. Without that guard they lock a freed mutex, which throws
std::system_error into HIP’s noexcept teardown and aborts the process.
What is tracked#
Each recorded pointer is classified by query_alloc(), which queries hsa_amd_pointer_info and
keeps the allocation only when it is coarse-grained and not kernarg. The exclusions are
load-bearing, not incidental:
Excluded |
Why |
|---|---|
Kernarg memory |
It holds the pointer arguments of kernels. A torn or stale restore landing on kernarg faults the GPU. |
Host and fine-grained memory |
Out of scope for replay and precarious to restore. |
Executable allocations |
HIP places its per-stream and per-graph kernarg pools — and rocprofiler its trace buffers — in the coarse-grained segment with the executable flag, so they would otherwise pass the kernarg check. They hold live kernel arguments and runtime state rather than application data; snapshotting them means a later |
Each surviving entry is tagged with its owning agent (hsa_amd_pointer_info::agentOwner), which is
what makes snapshots agent-scoped.
What snap() captures#
snap(agent) produces a device_snapshot_t holding one host-side copy per captured region. It
captures two categories:
Tracked allocations owned by
agent, as filtered above.Module-scope
__device__and__constant__variables visible toagent.
Capture is a plain device-to-host copy through hsa_memory_copy, taken from rocprofiler’s captured
HSA table so it uses the original, un-wrapped function.
Module-scope variables#
Module-scope variables are not hipMalloc allocations. They live in the loaded executable’s data
segment, so the allocation tracker never sees them — yet a kernel that mutates a __device__ global
would leak that mutation into the next pass. snap() therefore discovers them separately: it
iterates the loaded code objects and, for each one, calls hsa_executable_iterate_agent_symbols for
the replaying agent, collecting symbols of kind HSA_SYMBOL_KIND_VARIABLE along with their device
address and size. Symbols with a zero address or size are skipped, as is anything larger than 1 GiB
(a sanity cap).
Iterating for a specific agent naturally scopes the walk to executables loaded on that agent —
others yield no symbols — which matches snap()’s per-agent contract.
This discovery must run at snapshot time rather than at executable-load time, because constant memory is not necessarily populated when the executable loads.
Failure handling: an incomplete snapshot declines replay#
Capture of a single region fails if the host allocation fails under memory pressure (a std::bad_alloc
from the resize) or if the device-to-host copy fails. Either leaves the snapshot incomplete, and
snap() returns with ok == false.
Two whole-snapshot conditions produce the same result. Reserving the inventory metadata can itself
run out of host memory, and HSA may refuse to enumerate the module-scope variables of a loaded
executable. The second matters because a __device__ global that was never discovered is one that
is never restored, so passes 2…N would read state accumulated by pass 1 and report counters for
inputs that differ from the first pass. Neither is fatal: both log which condition was hit and
decline replay for that dispatch.
The replay loop treats that as a decision point, not an error to push through: restoring a partial
snapshot between passes would write back some regions and leave others carrying a later pass’s
mutations, corrupting application data. So on ok == false the loop warns, closes the CONFIG
callback sequence, and runs the dispatch exactly once with its original completion signal, still
under the agent writer lock. The application sees a normal, un-replayed execution.
What restore() writes back#
restore() copies each captured region back host-to-device and returns the number of regions
successfully restored. The two categories are handled differently:
Tracked allocations are re-validated first. The lookup and the copy happen together under the inventory read lock, so a concurrent free cannot race between the check and the write. If the pointer is no longer a live allocation of at least the captured size — freed or reallocated since the snapshot — the region is skipped with a warning rather than written to memory that no longer belongs to it.
Module-scope variables live in the loaded executable and are always present, so they are restored directly.
A failed individual copy is logged and skipped; it does not abort the replay.
HIP graphs#
HIP graph launches are not replayed. The interceptor warns once (process-wide) and the graph runs
once on the ordinary path. Excluding graph_launch_active from the replay gate is what makes that
a graceful decline rather than an abort: snapshot and restore around a graph’s runtime-managed
memory and ordering is undefined, so the dispatch is not replayed.
A graph with no other consumers runs un-replayed through the interceptor’s fast path. The warning is emitted once so a large graph workload does not flood the log.
Graph replay is future work.
Known limitations#
Stated plainly, because each one has a concrete cause in the mechanism above.
No dirty-page hashing. Every tracked byte is copied on every restore. Host-side and/or device-side hashing is expected later; see Snapshot mechanism.
Virtual-memory mappings are not tracked. The tracker wraps only the four allocate and free entry points listed above, plus the
hsa_amd_pointer_infoquery used to classify them. It does not wraphsa_amd_vmem_map/hsa_amd_vmem_unmapor any other part of the virtual-memory API. (Those functions do appear in the SDK’s HSA API tracing tables, but that is a separate mechanism and does not feed the replay inventory.) Anything that becomes addressable through avmemmapping rather than through one of the wrapped allocations is therefore invisible to the snapshot — which covershipMallocAsyncand other pool-backed, stream-ordered allocations. A kernel that writes such a buffer will not have those writes reverted between passes.Async SDMA copies are not fenced by the replay window.
hsa_amd_memory_async_copyand its variants are not kernel dispatches, so they bypass both the AQL queues and the per-agent replay gate. The source marks serializing them as a follow-up. See Concurrency and isolation.Coarse-grained device memory only. Kernarg, host, fine-grained and executable allocations are excluded by design, as is unified and managed memory.
HIP graph launches are not replayed (warn once, run once), as described above.
Only single-packet, single-dispatch submissions are replayed. The replay gate requires exactly one packet in the batch and exactly one dispatch packet in it; anything else takes the normal path.
The tracked device footprint is duplicated in host RAM for the lifetime of the replay, and every region is copied on every restore. Snapshot and restore cost scales linearly with the tracked footprint, so for large footprints
Npasses of snap plus restore can cost more than re-running the applicationNtimes.Single process. There is no cross-process or multi-process coordination, so replaying a kernel that participates in inter-process collectives is not safe.
Source reference#
All paths are relative to projects/rocprofiler-sdk/.
Component |
File |
Symbol |
|---|---|---|
Snapshot capture |
|
|
Module-variable discovery |
|
|
Restore |
|
|
Snapshot types |
|
|
HSA hook installation |
|
|
Allocation recording |
|
|
Agent-scoped inventory |
|
|
Trackability classifier |
|
|
Tracking activation |
|
|
Snapshot-declined fallback |
|
|
HIP graph warn-once + run-once |
|
|
Tests |
|
|