Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
beec6a3
fix(hrr): make the capture writer safe across fork and in the crash path
andraghetti Sep 25, 2026
7877d8d
fix(hrr): clear the emergency manifest flag in a forked child
andraghetti Sep 28, 2026
d9a8d5f
docs(hrr): state the writer lock order and what bounds the buffer wait
andraghetti Sep 29, 2026
771f55e
test(hrr): cover fork while recording and a crash on a small stack
andraghetti Oct 1, 2026
2261740
fix(hrr): reopen a forked child's archive on its first record
andraghetti Oct 1, 2026
0e379d5
fix(hrr): declare the fork reopen state apart from develop's new globals
andraghetti Oct 1, 2026
cb97fa4
fix(hrr): clear the inherited capture-incomplete flag in a forked child
andraghetti Oct 1, 2026
5918d50
fix(hrr): stop the fork-while-recording loop at the first hung child
andraghetti Oct 1, 2026
390067b
Merge branch 'develop' into users/andraghetti/hrr-capture-fork-crash
andraghetti Oct 5, 2026
403613f
test(hrr): cover the writer mutex across fork and a crash during fork
andraghetti Oct 5, 2026
3daf63e
fix(hrr): hold the unreplayable-API mutex across fork
andraghetti Oct 5, 2026
09c9d27
fix(hrr): keep a forked child from opening its archive after close
andraghetti Oct 5, 2026
a5fb668
fix(hrr): keep a child forked after close from opening an archive
andraghetti Oct 5, 2026
353fa9f
fix(hrr): finalize a forked child's archive only after it is open
andraghetti Oct 6, 2026
1260f51
Merge branch 'develop' into users/andraghetti/hrr-capture-fork-crash
andraghetti Oct 6, 2026
35226f2
test(hrr): record in the forked child of the disk-space fork tests
andraghetti Oct 6, 2026
9cc1ef7
Merge branch 'develop' into users/andraghetti/hrr-capture-fork-crash
andraghetti Oct 6, 2026
a966925
test(hrr): share one fsync between the fork and disk-space tests
andraghetti Oct 6, 2026
3d0e699
Merge branch 'develop' of github.com:ROCm/rocm-systems into HEAD
andraghetti Oct 7, 2026
7e625df
fix(hrr): drop a record that comes after the capture's trailer
andraghetti Oct 7, 2026
b63e660
fix(hrr): open no archive in a child forked after the capture's flush
andraghetti Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
317 changes: 207 additions & 110 deletions projects/clr/hipamd/src/hrr/hip_capture_writer.cpp

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions projects/hrr/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,11 @@ processes. Every HIP-owning process writes an independent sub-archive at
two live writers into one `events.bin` without needing an advisory lock.

`writer::open()` (`hip_capture_writer.cpp`) always selects the current process's
PID directory. A `fork()` child re-opens from the base dir in `atfork_child`, so
the child naturally switches to its own `pid-<childpid>/` sub-archive. The root
PID directory. A `fork()` child re-opens from the base dir on its first record,
blob or code object after the fork, so it switches to its own `pid-<childpid>/`
sub-archive. `atfork_child` only drops the parent's events fd and paths: the
child of a multithreaded process may make only async-signal-safe calls until it
execs, and a child that execs or exits without recording leaves no archive. The root
`manifest.json` is a common aggregate index with the schema fields
`version`, `capture_mode`, `owner_pid`, and `processes[]`; each process rewrites
it best-effort on clean shutdown by scanning existing `pid-*/manifest.json`
Expand Down
6 changes: 6 additions & 0 deletions projects/hrr/tests/hrr_test_common.hh
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,12 @@ inline void hrr_capture_direct(const std::string& direct_case,
}
#endif // HRR_TEST_EXE

// How many children Unit_HRR_ForkWhileRecording_Direct forks. Every other
// child records a call before it exits, starting with the first, and the
// roundtrip expects one archive for each of those besides the parent's.
inline constexpr int kHrrForkWhileRecordingForks = 200;
inline constexpr int kHrrForkWhileRecordingArchives = 1 + (kHrrForkWhileRecordingForks + 1) / 2;

inline std::pair<int, std::string> hrr_playback_env(
const fs::path& cap_path,
const std::vector<std::pair<std::string, std::string>>& env,
Expand Down
5 changes: 5 additions & 0 deletions projects/hrr/tests/integration/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
# fake a small file system under HIP_HRR_CAPTURE_OUTPUT when the test sets
# HRR_TEST_FAKE_FS_TOTAL; every other call goes on to libc. Contains its own
# hidden [.][hrr-direct] workloads. POSIX only.
#
# hrr_clock_hook.cc defines clock_gettime for the binary, so a workload can run
# a hook on one thread's clock reads, as the capture writer timestamps a record.
# POSIX only.

# The workload translation units carry __global__ kernels, device built-ins
# (blockIdx/blockDim/threadIdx), hipLaunchKernelGGL, and HIP_SYMBOL, so they must
Expand Down Expand Up @@ -74,6 +78,7 @@ add_executable(hrr-integration-tests
hrr_roundtrip_test.cc
hrr_api_matrix_test.cc
hrr_secure_exec_test.cc
hrr_clock_hook.cc
)
set_target_properties(hrr-integration-tests PROPERTIES
CXX_STANDARD 17
Expand Down
20 changes: 20 additions & 0 deletions projects/hrr/tests/integration/hrr_clock_hook.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
*
* SPDX-License-Identifier: MIT
*/

#include "hrr_clock_hook.hh"

#ifndef _WIN32
#include <sys/syscall.h>
#include <time.h>
#include <unistd.h>

thread_local void (*t_hrr_clock_hook)() = nullptr;

extern "C" int clock_gettime(clockid_t clk, struct timespec* ts) noexcept {
if (auto hook = t_hrr_clock_hook) hook();
return static_cast<int>(syscall(SYS_clock_gettime, clk, ts));
}
#endif // !_WIN32
16 changes: 16 additions & 0 deletions projects/hrr/tests/integration/hrr_clock_hook.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
*
* SPDX-License-Identifier: MIT
*/

#pragma once

// The test binary defines clock_gettime() itself, in hrr_clock_hook.cc. The
// dynamic linker binds the HIP runtime's calls to it before libc's. The capture
// writer calls it to timestamp each record, before it takes the buffer lock.
//
// A thread that sets t_hrr_clock_hook gets it called on each clock_gettime()
// that thread makes, before the clock is read. Other threads are not affected.
// POSIX only.
extern thread_local void (*t_hrr_clock_hook)();
18 changes: 18 additions & 0 deletions projects/hrr/tests/integration/hrr_disk_space_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,11 @@ extern "C" int hrr_disk_space_statvfs64(const char* path, struct statvfs* buf) {
return fake_statvfs("statvfs64", path, buf);
}

// Defined in hrr_workload_test.cc: the fork cases there hold a thread in fsync.
extern "C" void hrr_workload_fsync_hook(int fd);

extern "C" int hrr_disk_space_fsync(int fd) {
hrr_workload_fsync_hook(fd);
using Fn = int (*)(int);
static Fn real = reinterpret_cast<Fn>(dlsym(RTLD_NEXT, "fsync"));
if (g_fsync_hold.exchange(false)) {
Expand Down Expand Up @@ -265,6 +269,17 @@ void copy_to_device(void* dev, uint32_t seed, size_t len) {
HRR_HIP_CHECK(hipMemcpy(dev, host.data(), len, hipMemcpyHostToDevice));
}

// A forked child opens its archive on its first record, not at fork. This
// records one without touching the device, which a forked child must not use:
// a launch configuration pushed and popped through the compiler dispatch table.
void record_in_child() {
dim3 grid, block;
size_t shared = 0;
hipStream_t stream = nullptr;
(void)__hipPushCallConfiguration(dim3(1), dim3(1), 0, nullptr);
(void)__hipPopCallConfiguration(&grid, &block, &shared, &stream);
}

// Wait for a forked child for at most 60 s; -1 when it had to be killed.
int wait_child(pid_t child) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60);
Expand Down Expand Up @@ -389,6 +404,7 @@ TEST_CASE("Unit_HRR_DiskSpace_ForkAfterStop_Direct", "[.][hrr-direct]") {
const pid_t child = fork();
if (child == 0) {
g_quick_exit_pid.store(getpid());
record_in_child();
std::exit(0);
}
REQUIRE(child > 0);
Expand Down Expand Up @@ -417,6 +433,8 @@ TEST_CASE("Unit_HRR_DiskSpace_RefusedForkChild_Direct", "[.][hrr-direct]") {
const pid_t child = fork();
if (child == 0) {
g_quick_exit_pid.store(getpid());
// Its open, and so the refusal, comes with its first record.
record_in_child();
std::exit(0);
}
REQUIRE(child > 0);
Expand Down
Loading
Loading