Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

505 lines
16 KiB
C++

// Copyright (c) 2026 Advanced Micro Devices, Inc.
// SPDX-License-Identifier: MIT
#include <gtest/gtest.h>
#include "rocjitsu/base/rj_compiler.h"
#include "rocjitsu/kmd/linux/rpc.h"
RJ_DIAGNOSTIC_PUSH
RJ_DIAGNOSTIC_IGNORE_PEDANTIC
#include "linux/uapi/kfd_ioctl.h"
RJ_DIAGNOSTIC_POP
#include <atomic>
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <dirent.h>
#include <fcntl.h>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <thread>
#include <unistd.h>
#include <vector>
namespace {
constexpr const char *kKfdPath = "/dev/kfd";
constexpr const char *kTopologyNodesPaths[] = {
"/sys/devices/virtual/kfd/kfd/topology/nodes",
"/sys/class/kfd/kfd/topology/nodes",
};
constexpr uint32_t kGuestGpuId = 38144;
constexpr int kChildCount = 4;
constexpr int kStressThreadCount = 8;
constexpr int kStressIterations = 25;
constexpr uint64_t kAllocVa = 0x1000000000ULL;
constexpr uint64_t kAllocSize = 4096;
// Resolve the directory holding the config-path handoff file, mirroring the exact
// precedence in load_dbt_guest_config_from_runtime_config() so a config the test
// installs is the one the in-process DBT guest hook actually reads:
// 1. $ROCJITSU_INVOCATION_DIR (the launcher's per-invocation dir), then
// 2. <$ROCJITSU_RUNTIME_DIR>/<pid>/ (the per-PID tier the reader probes next;
// the test and the hook share this process, so getpid() matches), then
// 3. <$ROCJITSU_RUNTIME_DIR>/ (the well-known location for the no-launcher case).
// The reader opens the FIRST tier whose config_path handoff actually exists, so this
// must probe them in the same order rather than short-circuiting on tier 1 merely
// being set: an $ROCJITSU_INVOCATION_DIR pointing at a dir without config_path must
// still fall through to the per-PID/base tiers a valid handoff may live in. When no
// tier has a handoff yet, return the highest-priority writable tier so
// install_inline_dbt_config() writes where the reader looks first.
// rpc_default_runtime_dir() already treats an unset OR empty $ROCJITSU_RUNTIME_DIR as
// "use $XDG_RUNTIME_DIR/rocjitsu or /tmp/rocjitsu-<uid>", so this never returns
// nullopt on that account — otherwise the test would write nowhere while the runtime
// hook still reads the default location.
std::optional<std::filesystem::path> config_handoff_dir() {
std::vector<std::filesystem::path> tiers;
if (const char *inv = std::getenv("ROCJITSU_INVOCATION_DIR"); inv && *inv)
tiers.emplace_back(inv);
const std::filesystem::path base(rocjitsu::rpc_default_runtime_dir());
tiers.push_back(base / std::to_string(getpid()));
tiers.push_back(base);
std::error_code ec;
for (const auto &tier : tiers) {
if (std::filesystem::exists(tier / "config_path", ec))
return tier;
}
// No handoff exists yet: install into the highest-priority tier the reader probes.
return tiers.front();
}
std::optional<std::string> read_active_config_json() {
const auto dir = config_handoff_dir();
if (!dir)
return std::nullopt;
std::ifstream active_config(*dir / "config_path");
std::string configured_path;
if (!std::getline(active_config, configured_path) || configured_path.empty())
return std::nullopt;
std::ifstream config(std::filesystem::absolute(configured_path).lexically_normal());
if (!config)
return std::nullopt;
return std::string(std::istreambuf_iterator<char>(config), std::istreambuf_iterator<char>());
}
bool install_inline_dbt_config(std::string simulator_json, const char *host_isa,
uint32_t lds_size_kb, std::string_view external_host_config = {}) {
const auto dir = config_handoff_dir();
if (!dir)
return false;
try {
const std::filesystem::path runtime(*dir);
std::filesystem::create_directories(runtime);
const std::filesystem::path config_path = runtime / "inline_dbt_failure_config.json";
const size_t insert_pos = simulator_json.find('{');
if (insert_pos == std::string::npos)
return false;
std::ostringstream dbt_guest;
dbt_guest << R"(
"dbt_guest": {
"enabled": true,
"guest_isa": "gfx950",
"host_isa": ")"
<< host_isa << R"(",
"execution_backend": "simulator",)";
if (!external_host_config.empty())
dbt_guest << R"(
"simulator_config": ")"
<< external_host_config << R"(",)";
dbt_guest << R"(
"guest_device": {
"gpu_id": 38144,
"gfx_target_version": 90500,
"simd_count": 64,
"max_waves_per_simd": 8,
"num_shader_engines": 4,
"num_cu_per_sh": 4,
"simd_per_cu": 4,
"wave_front_size": 64,
"max_slots_scratch_cu": 32,
"lds_size_kb": )"
<< lds_size_kb << R"(
}
},)";
simulator_json.insert(insert_pos + 1, dbt_guest.str());
std::ofstream config(config_path);
if (!config)
return false;
config << simulator_json;
config.close();
if (!config)
return false;
std::ofstream active_config(runtime / "config_path");
if (!active_config)
return false;
active_config << config_path.string() << '\n';
return active_config.good();
} catch (const std::filesystem::filesystem_error &) {
return false;
}
}
bool read_gpu_id(const std::string &path, uint32_t *gpu_id) {
FILE *file = fopen(path.c_str(), "r");
if (!file)
return false;
unsigned parsed = 0;
const int scanned = fscanf(file, "%u", &parsed);
fclose(file);
if (scanned != 1)
return false;
*gpu_id = static_cast<uint32_t>(parsed);
return true;
}
std::string topology_nodes_path() {
for (const char *path : kTopologyNodesPaths) {
if (access(path, R_OK | X_OK) == 0)
return path;
}
return {};
}
bool guest_gpu_is_visible() {
std::string nodes_path = topology_nodes_path();
if (nodes_path.empty())
return false;
DIR *dir = opendir(nodes_path.c_str());
if (!dir)
return false;
bool found = false;
while (auto *entry = readdir(dir)) {
if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0)
continue;
uint32_t gpu_id = 0;
std::string gpu_id_path = nodes_path + "/" + entry->d_name + "/gpu_id";
if (read_gpu_id(gpu_id_path, &gpu_id) && gpu_id == kGuestGpuId) {
found = true;
break;
}
}
closedir(dir);
return found;
}
bool read_process_aperture_count(int fd, uint32_t *count) {
kfd_ioctl_get_process_apertures_new_args args{};
if (ioctl(fd, AMDKFD_IOC_GET_PROCESS_APERTURES_NEW, &args) != 0)
return false;
*count = args.num_of_nodes;
return true;
}
int child_process() {
int fd = open(kKfdPath, O_RDWR | O_CLOEXEC);
if (fd < 0)
return 2;
const bool visible = guest_gpu_is_visible();
close(fd);
return visible ? 0 : 3;
}
int child_reset_process(int inherited_fd, uint32_t expected_reopened_count) {
uint32_t inherited_count = 0;
if (read_process_aperture_count(inherited_fd, &inherited_count))
return 7;
int fd = open(kKfdPath, O_RDWR | O_CLOEXEC);
if (fd < 0)
return 2;
uint32_t reopened_count = 0;
const bool count_ok = read_process_aperture_count(fd, &reopened_count);
const bool visible_after_reopen = guest_gpu_is_visible();
close(fd);
if (!count_ok)
return 5;
if (reopened_count < expected_reopened_count)
return 6;
return visible_after_reopen ? 0 : 3;
}
TEST(GuestKfdMultiprocessTest, ForkedChildrenDoNotRemoveParentOverlay) {
if (access(kKfdPath, R_OK | W_OK) != 0)
GTEST_SKIP() << kKfdPath << " is not available: " << std::strerror(errno);
int parent_fd = open(kKfdPath, O_RDWR | O_CLOEXEC);
ASSERT_GE(parent_fd, 0);
if (!guest_gpu_is_visible()) {
close(parent_fd);
FAIL() << "guest GPU overlay is not visible";
}
std::vector<pid_t> children;
children.reserve(kChildCount);
for (int i = 0; i < kChildCount; ++i) {
pid_t pid = fork();
ASSERT_GE(pid, 0) << "fork failed: " << std::strerror(errno);
if (pid == 0)
_exit(child_process());
children.push_back(pid);
}
for (pid_t pid : children) {
int status = 0;
ASSERT_EQ(waitpid(pid, &status, 0), pid);
ASSERT_TRUE(WIFEXITED(status)) << "child " << pid << " did not exit normally";
EXPECT_EQ(WEXITSTATUS(status), 0) << "child " << pid << " failed";
}
EXPECT_TRUE(guest_gpu_is_visible()) << "parent guest topology disappeared after forked children";
close(parent_fd);
}
TEST(GuestKfdMultiprocessTest, ForkedChildDropsInheritedGuestDriverBeforeReopen) {
if (access(kKfdPath, R_OK | W_OK) != 0)
GTEST_SKIP() << kKfdPath << " is not available: " << std::strerror(errno);
int parent_fd = open(kKfdPath, O_RDWR | O_CLOEXEC);
ASSERT_GE(parent_fd, 0);
if (!guest_gpu_is_visible()) {
close(parent_fd);
FAIL() << "guest GPU overlay is not visible";
}
uint32_t parent_count_before = 0;
ASSERT_TRUE(read_process_aperture_count(parent_fd, &parent_count_before));
pid_t pid = fork();
ASSERT_GE(pid, 0) << "fork failed: " << std::strerror(errno);
if (pid == 0)
_exit(child_reset_process(parent_fd, parent_count_before));
int status = 0;
ASSERT_EQ(waitpid(pid, &status, 0), pid);
ASSERT_TRUE(WIFEXITED(status)) << "child " << pid << " did not exit normally";
EXPECT_EQ(WEXITSTATUS(status), 0) << "child " << pid << " did not reset and reopen cleanly";
uint32_t parent_count_after = 0;
EXPECT_TRUE(read_process_aperture_count(parent_fd, &parent_count_after));
EXPECT_EQ(parent_count_after, parent_count_before);
EXPECT_TRUE(guest_gpu_is_visible()) << "parent guest topology disappeared after child reset";
close(parent_fd);
}
TEST(GuestKfdConcurrencyTest, ConcurrentOpenIoctlAndSysfsRedirect) {
if (access(kKfdPath, R_OK | W_OK) != 0)
GTEST_SKIP() << kKfdPath << " is not available: " << std::strerror(errno);
int probe_fd = open(kKfdPath, O_RDWR | O_CLOEXEC);
ASSERT_GE(probe_fd, 0);
if (!guest_gpu_is_visible()) {
close(probe_fd);
FAIL() << "guest GPU overlay is not visible";
}
close(probe_fd);
std::atomic<int> open_failures{0};
std::atomic<int> ioctl_failures{0};
std::atomic<int> sysfs_failures{0};
std::vector<std::thread> threads;
threads.reserve(kStressThreadCount);
for (int thread_idx = 0; thread_idx < kStressThreadCount; ++thread_idx) {
threads.emplace_back([&]() {
for (int iter = 0; iter < kStressIterations; ++iter) {
int fd = open(kKfdPath, O_RDWR | O_CLOEXEC);
if (fd < 0) {
open_failures.fetch_add(1, std::memory_order_relaxed);
continue;
}
kfd_ioctl_get_version_args version{};
if (ioctl(fd, AMDKFD_IOC_GET_VERSION, &version) != 0)
ioctl_failures.fetch_add(1, std::memory_order_relaxed);
if (!guest_gpu_is_visible())
sysfs_failures.fetch_add(1, std::memory_order_relaxed);
close(fd);
}
});
}
for (auto &thread : threads)
thread.join();
EXPECT_EQ(open_failures.load(std::memory_order_relaxed), 0);
EXPECT_EQ(ioctl_failures.load(std::memory_order_relaxed), 0);
EXPECT_EQ(sysfs_failures.load(std::memory_order_relaxed), 0);
}
TEST(GuestKfdMemoryTest, GuestAllocationMmapOffsetIsRejected) {
if (access(kKfdPath, R_OK | W_OK) != 0)
GTEST_SKIP() << kKfdPath << " is not available: " << std::strerror(errno);
int fd = open(kKfdPath, O_RDWR | O_CLOEXEC);
ASSERT_GE(fd, 0);
if (!guest_gpu_is_visible()) {
close(fd);
FAIL() << "guest GPU overlay is not visible";
}
kfd_ioctl_alloc_memory_of_gpu_args alloc{};
alloc.va_addr = kAllocVa;
alloc.size = kAllocSize;
alloc.gpu_id = kGuestGpuId;
alloc.flags = KFD_IOC_ALLOC_MEM_FLAGS_VRAM | KFD_IOC_ALLOC_MEM_FLAGS_WRITABLE;
ASSERT_EQ(ioctl(fd, AMDKFD_IOC_ALLOC_MEMORY_OF_GPU, &alloc), 0) << std::strerror(errno);
EXPECT_NE(alloc.handle, 0u);
EXPECT_NE(alloc.mmap_offset, 0u);
errno = 0;
void *mapped = mmap(nullptr, kAllocSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd,
static_cast<off_t>(alloc.mmap_offset));
if (mapped != MAP_FAILED) {
munmap(mapped, kAllocSize);
ADD_FAILURE() << "guest synthetic allocation mmap unexpectedly succeeded";
} else {
EXPECT_EQ(errno, ENODEV);
}
kfd_ioctl_free_memory_of_gpu_args free_args{};
free_args.handle = alloc.handle;
EXPECT_EQ(ioctl(fd, AMDKFD_IOC_FREE_MEMORY_OF_GPU, &free_args), 0) << std::strerror(errno);
close(fd);
}
TEST(GuestKfdFailureTest, NonexistentSimulatorConfigFailsCleanly) {
const auto dir = config_handoff_dir();
ASSERT_TRUE(dir.has_value());
const std::string nonexistent = (*dir / "nonexistent_simulator_config.json").string();
ASSERT_TRUE(install_inline_dbt_config(R"({"max_ticks": 1})", "gfx942", 64, nonexistent));
errno = 0;
const int fd = open(kKfdPath, O_RDWR | O_CLOEXEC);
EXPECT_EQ(fd, -1);
EXPECT_EQ(errno, ENODEV);
if (fd >= 0)
close(fd);
}
TEST(GuestKfdFailureTest, SimulatorHostIsaMismatchFailsCleanly) {
const auto simulator_json = read_active_config_json();
ASSERT_TRUE(simulator_json.has_value());
ASSERT_TRUE(install_inline_dbt_config(*simulator_json, "gfx940", 64));
errno = 0;
const int fd = open(kKfdPath, O_RDWR | O_CLOEXEC);
EXPECT_EQ(fd, -1);
EXPECT_EQ(errno, ENODEV);
if (fd >= 0)
close(fd);
}
TEST(GuestKfdFailureTest, SimulatorOversizedLdsFailsCleanly) {
const auto simulator_json = read_active_config_json();
ASSERT_TRUE(simulator_json.has_value());
ASSERT_TRUE(install_inline_dbt_config(*simulator_json, "gfx942", 65));
errno = 0;
const int fd = open(kKfdPath, O_RDWR | O_CLOEXEC);
EXPECT_EQ(fd, -1);
EXPECT_EQ(errno, ENODEV);
if (fd >= 0)
close(fd);
}
// Overwriting the interposer's hidden real /dev/kfd fd number via dup2 must not
// leave a stale KFD classification behind. In guest/DBT mode open("/dev/kfd")
// returns an app-facing dup while the real fd stays internal; if some dup2/dup3
// target reuses that hidden number, the interposer must stop routing KFD ioctls
// on it (GuestKfd::invalidate_primary_fd clears real_kfd_fd_ + ready_), and app
// dups plus fresh opens must keep working via a lazily reopened real fd.
//
// The hidden fd number is not directly observable, so this brute-forces the
// candidate space: it dup2's a pipe end over every low fd number (skipping the
// ones the test itself owns). Whichever number was the hidden real fd gets
// overwritten; the invariant under test is that KFD routing stays correct
// regardless of which number that was.
TEST(GuestKfdFdReuseTest, OverwritingHiddenRealFdKeepsRoutingCorrect) {
if (access(kKfdPath, R_OK | W_OK) != 0)
GTEST_SKIP() << kKfdPath << " is not available: " << std::strerror(errno);
auto version_ok = [](int f) {
kfd_ioctl_get_version_args version{};
return ioctl(f, AMDKFD_IOC_GET_VERSION, &version) == 0;
};
int app_fd = open(kKfdPath, O_RDWR | O_CLOEXEC);
ASSERT_GE(app_fd, 0);
if (!guest_gpu_is_visible()) {
close(app_fd);
FAIL() << "guest GPU overlay is not visible";
}
ASSERT_TRUE(version_ok(app_fd));
// A second app-facing dup holds the guest process open across the overwrite.
int keeper = dup(app_fd);
ASSERT_GE(keeper, 0);
ASSERT_TRUE(version_ok(keeper));
int pipefd[2];
ASSERT_EQ(pipe(pipefd), 0);
// Overwrite every low fd number except the ones this test owns. Whichever
// number is the interposer's hidden real /dev/kfd fd gets clobbered here,
// driving invalidate_primary_fd() on that backend.
constexpr int kMaxProbeFd = 64;
for (int target = 3; target < kMaxProbeFd; ++target) {
if (target == app_fd || target == keeper || target == pipefd[0] || target == pipefd[1])
continue;
// dup2 onto an unopened number just creates a new dup of the pipe read end;
// onto an open number it atomically closes the old one first. Either way the
// target now names the pipe, not KFD.
if (dup2(pipefd[0], target) != target)
continue;
// The overwritten number must never route KFD ioctls.
EXPECT_FALSE(version_ok(target)) << "fd " << target << " still routed KFD after dup2 overwrite";
close(target);
}
// The surviving app dup must still route KFD, and a fresh open must succeed
// (the interposer lazily reopens the hidden real fd if it was the clobbered
// number), still seeing the guest overlay.
EXPECT_TRUE(version_ok(keeper)) << "keeper dup stopped routing after hidden-fd overwrite";
int reopened = open(kKfdPath, O_RDWR | O_CLOEXEC);
ASSERT_GE(reopened, 0) << "reopen after hidden-fd overwrite failed: " << std::strerror(errno);
EXPECT_TRUE(version_ok(reopened));
EXPECT_TRUE(guest_gpu_is_visible());
close(reopened);
close(keeper);
close(app_fd);
close(pipefd[0]);
close(pipefd[1]);
}
} // namespace