Files
TheRock/rocm-systems/emulation/rocjitsu/tests/l2_cache_test.cpp
T
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

1239 lines
45 KiB
C++

// Copyright (c) 2026 Advanced Micro Devices, Inc.
// SPDX-License-Identifier: MIT
#include "rocjitsu/vm/amdgpu/gpu_memory.h"
#include "rocjitsu/vm/amdgpu/hbm_controller.h"
#include "rocjitsu/vm/amdgpu/l1_scalar_cache.h"
#include "rocjitsu/vm/amdgpu/l1_vector_cache.h"
#include "rocjitsu/vm/amdgpu/l2_cache.h"
#include "rocjitsu/vm/amdgpu/memory_side_cache.h"
#include "simdojo/sim/exec_mode.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <barrier>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <limits>
#include <linux/memfd.h>
#include <memory>
#include <span>
#include <string>
#include <string_view>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <thread>
#include <unistd.h>
#include <vector>
namespace {
using rocjitsu::amdgpu::GpuMemory;
using rocjitsu::amdgpu::HbmController;
using rocjitsu::amdgpu::L1ScalarCache;
using rocjitsu::amdgpu::L1VectorCache;
using rocjitsu::amdgpu::L2Cache;
using rocjitsu::amdgpu::MemorySideCache;
using rocjitsu::amdgpu::Mtype;
void increment_u32(uint8_t *line, uint32_t offset) {
uint32_t value = 0;
std::memcpy(&value, line + offset, sizeof(value));
++value;
std::memcpy(line + offset, &value, sizeof(value));
}
void report_benchmark(std::string_view name, uint64_t operations,
std::chrono::steady_clock::duration elapsed) {
const double total_ns = std::chrono::duration<double, std::nano>(elapsed).count();
std::cout << "ROCJITSU_BENCHMARK" << " name=" << name << " operations=" << operations
<< " total_ns=" << static_cast<uint64_t>(total_ns) << " ns_per_op=" << std::fixed
<< std::setprecision(3) << total_ns / static_cast<double>(operations) << '\n';
}
void run_cross_l2_atomic_benchmark(std::string_view name, bool same_address) {
GpuMemory memory("memory");
L2Cache l2a("l2a");
L2Cache l2b("l2b");
l2a.set_backing_memory(&memory);
l2b.set_backing_memory(&memory);
constexpr uint32_t kThreads = 8;
constexpr uint32_t kIterations = 25'000;
constexpr uint64_t kBase = 0x800000;
constexpr uint64_t kOperations = static_cast<uint64_t>(kThreads) * kIterations;
for (uint32_t tid = 0; tid < kThreads; ++tid)
memory.write32(kBase + static_cast<uint64_t>(tid) * L2Cache::LINE_SIZE, 0);
std::barrier ready(kThreads + 1);
std::barrier start(kThreads + 1);
std::barrier done(kThreads + 1);
std::array<L2Cache *, 2> l2s = {&l2a, &l2b};
std::vector<std::thread> workers;
workers.reserve(kThreads);
for (uint32_t tid = 0; tid < kThreads; ++tid) {
workers.emplace_back([&, tid] {
L2Cache *l2 = l2s[tid % l2s.size()];
const uint64_t target =
kBase + (same_address ? 0 : static_cast<uint64_t>(tid) * L2Cache::LINE_SIZE);
ready.arrive_and_wait();
start.arrive_and_wait();
for (uint32_t iteration = 0; iteration < kIterations; ++iteration)
l2->atomic_rmw(target, sizeof(uint32_t), increment_u32);
done.arrive_and_wait();
});
}
ready.arrive_and_wait();
const auto begin = std::chrono::steady_clock::now();
start.arrive_and_wait();
done.arrive_and_wait();
const auto end = std::chrono::steady_clock::now();
for (auto &worker : workers)
worker.join();
if (same_address) {
EXPECT_EQ(memory.read32(kBase), kOperations);
} else {
uint64_t observed_operations = 0;
for (uint32_t tid = 0; tid < kThreads; ++tid) {
const uint32_t observed =
memory.read32(kBase + static_cast<uint64_t>(tid) * L2Cache::LINE_SIZE);
EXPECT_EQ(observed, kIterations) << "thread=" << tid;
observed_operations += observed;
}
EXPECT_EQ(observed_operations, kOperations);
}
report_benchmark(name, kOperations, end - begin);
}
void run_atomic_hierarchy_benchmark(std::string_view name, uint32_t hierarchy_count) {
GpuMemory memory("memory");
std::vector<std::unique_ptr<L2Cache>> l2s;
std::vector<std::unique_ptr<L1ScalarCache>> scalar_l1s;
std::vector<std::unique_ptr<L1VectorCache>> vector_l1s;
for (uint32_t i = 0; i < hierarchy_count; ++i) {
auto l2 = std::make_unique<L2Cache>("l2_" + std::to_string(i));
l2->set_backing_memory(&memory);
scalar_l1s.push_back(std::make_unique<L1ScalarCache>(l2.get()));
vector_l1s.push_back(std::make_unique<L1VectorCache>(l2.get()));
l2s.push_back(std::move(l2));
}
constexpr uint64_t kAddr = 0x880000;
constexpr uint32_t kIterations = 100'000;
memory.write32(kAddr, 0);
const auto begin = std::chrono::steady_clock::now();
for (uint32_t i = 0; i < kIterations; ++i)
l2s.front()->atomic_rmw(kAddr, sizeof(uint32_t), increment_u32);
const auto end = std::chrono::steady_clock::now();
EXPECT_EQ(memory.read32(kAddr), kIterations);
report_benchmark(name, kIterations, end - begin);
}
void run_scalar_l1_hit_benchmark() {
GpuMemory memory("memory");
L2Cache l2("l2");
l2.set_backing_memory(&memory);
L1ScalarCache l1(&l2);
constexpr uint64_t kAddr = 0x890000;
constexpr uint32_t kIterations = 1'000'000;
memory.write32(kAddr, 17);
uint32_t value = 0;
l1.load(kAddr, 1, &value);
const auto begin = std::chrono::steady_clock::now();
for (uint32_t i = 0; i < kIterations; ++i)
l1.load(kAddr, 1, &value);
const auto end = std::chrono::steady_clock::now();
EXPECT_EQ(value, 17u);
report_benchmark("scalar_l1_hit", kIterations, end - begin);
}
void run_vector_l1_hit_benchmark() {
GpuMemory memory("memory");
L2Cache l2("l2");
l2.set_backing_memory(&memory);
L1VectorCache l1(&l2);
constexpr uint64_t kAddr = 0x8A0000;
constexpr uint32_t kIterations = 500'000;
memory.write32(kAddr, 23);
const uint64_t addrs[1] = {kAddr};
std::array<uint8_t, sizeof(uint32_t)> value{};
l1.load(addrs, 1, sizeof(uint32_t), 1, value.data(), Mtype::RW, false, false, 1);
const auto begin = std::chrono::steady_clock::now();
for (uint32_t i = 0; i < kIterations; ++i)
l1.load(addrs, 1, sizeof(uint32_t), 1, value.data(), Mtype::RW, false, false, 1);
const auto end = std::chrono::steady_clock::now();
uint32_t observed = 0;
std::memcpy(&observed, value.data(), sizeof(observed));
EXPECT_EQ(observed, 23u);
report_benchmark("vector_l1_hit", kIterations, end - begin);
}
void run_invalidate_range_benchmark(std::string_view name, uint32_t lines, uint32_t iterations) {
GpuMemory memory("memory");
L2Cache l2("l2");
l2.set_backing_memory(&memory);
constexpr uint64_t kBase = 0xa00000;
std::array<uint8_t, L2Cache::LINE_SIZE> replacement{};
std::array<uint8_t, L2Cache::LINE_SIZE> actual{};
for (uint32_t line = 0; line < lines; ++line) {
const uint64_t address = kBase + static_cast<uint64_t>(line) * L2Cache::LINE_SIZE;
replacement.fill(static_cast<uint8_t>(line + 0x40));
memory.write_block(address, std::span<const uint8_t>(replacement));
}
std::chrono::steady_clock::duration elapsed{};
uint64_t refill_checksum = 0;
uint64_t expected_refill_checksum = 0;
for (uint32_t line = 0; line < lines; ++line)
expected_refill_checksum += static_cast<uint8_t>(line + 0x40);
expected_refill_checksum *= iterations;
for (uint32_t iteration = 0; iteration < iterations; ++iteration) {
// Refill outside the timed interval so every measured call invalidates
// resident lines rather than repeatedly walking an empty cache.
for (uint32_t line = 0; line < lines; ++line) {
const uint64_t address = kBase + static_cast<uint64_t>(line) * L2Cache::LINE_SIZE;
l2.read(address, actual.data(), actual.size());
refill_checksum += actual.front();
}
const auto begin = std::chrono::steady_clock::now();
l2.invalidate_range(kBase, static_cast<uint64_t>(lines) * L2Cache::LINE_SIZE, 0);
elapsed += std::chrono::steady_clock::now() - begin;
}
uint64_t checksum = 0;
uint64_t expected_checksum = 0;
for (uint32_t line = 0; line < lines; ++line) {
const uint64_t address = kBase + static_cast<uint64_t>(line) * L2Cache::LINE_SIZE;
replacement.fill(static_cast<uint8_t>(line + 0x40));
l2.read(address, actual.data(), actual.size());
EXPECT_EQ(actual, replacement) << "line=" << line;
checksum += actual.front();
expected_checksum += replacement.front();
}
EXPECT_EQ(refill_checksum, expected_refill_checksum);
EXPECT_EQ(checksum, expected_checksum);
report_benchmark(name, iterations, elapsed);
}
class ScopedFd {
public:
explicit ScopedFd(int fd) : fd_(fd) {}
ScopedFd(const ScopedFd &) = delete;
ScopedFd &operator=(const ScopedFd &) = delete;
~ScopedFd() {
if (fd_ >= 0)
close(fd_);
}
int get() const { return fd_; }
private:
int fd_;
};
class ScopedMapping {
public:
ScopedMapping(void *address, size_t size) : address_(address), size_(size) {}
ScopedMapping(const ScopedMapping &) = delete;
ScopedMapping &operator=(const ScopedMapping &) = delete;
~ScopedMapping() {
if (address_ != MAP_FAILED)
munmap(address_, size_);
}
uint8_t *data() const { return static_cast<uint8_t *>(address_); }
private:
void *address_;
size_t size_;
};
class FunctionalMemoryPort : public simdojo::Component {
public:
FunctionalMemoryPort(std::string name, uint64_t base, size_t size)
: simdojo::Component(std::move(name)), base_(base), bytes_(size) {
port_ = add_port(std::make_unique<simdojo::Port>("memory", 0, this, simdojo::PortDirection::IN,
simdojo::PortProtocol::MEMORY));
port_->set_handler([this](simdojo::Tick, simdojo::Message *message) {
assert(message != nullptr);
const auto &header = message->header();
assert(header.addr >= base_);
const uint64_t offset = header.addr - base_;
assert(offset + header.size_bytes <= bytes_.size());
auto *payload = reinterpret_cast<uint8_t *>(message->payload());
assert(payload != nullptr);
if (header.op == simdojo::MessageOp::READ) {
std::memcpy(payload, bytes_.data() + offset, header.size_bytes);
} else {
assert(header.op == simdojo::MessageOp::WRITE);
std::memcpy(bytes_.data() + offset, payload, header.size_bytes);
}
});
}
simdojo::Port *port() { return port_; }
void write32(uint64_t addr, uint32_t value) {
assert(addr >= base_ && addr - base_ + sizeof(value) <= bytes_.size());
std::memcpy(bytes_.data() + (addr - base_), &value, sizeof(value));
}
uint32_t read32(uint64_t addr) const {
assert(addr >= base_ && addr - base_ + sizeof(uint32_t) <= bytes_.size());
uint32_t value = 0;
std::memcpy(&value, bytes_.data() + (addr - base_), sizeof(value));
return value;
}
uint8_t read8(uint64_t addr) const {
assert(addr >= base_ && addr - base_ < bytes_.size());
return bytes_[addr - base_];
}
void write8(uint64_t addr, uint8_t value) {
assert(addr >= base_ && addr - base_ < bytes_.size());
bytes_[addr - base_] = value;
}
private:
uint64_t base_;
std::vector<uint8_t> bytes_;
simdojo::Port *port_ = nullptr;
};
TEST(L2CacheThreadingTest, ConcurrentDifferentSetWritesArePreserved) {
GpuMemory memory("memory");
L2Cache l2("l2");
l2.set_backing_memory(&memory);
constexpr uint32_t kThreads = 8;
constexpr uint32_t kLinesPerThread = 128;
constexpr uint64_t kBase = 0x100000;
std::barrier start(kThreads);
std::vector<std::thread> workers;
workers.reserve(kThreads);
for (uint32_t tid = 0; tid < kThreads; ++tid) {
workers.emplace_back([&, tid] {
std::array<uint8_t, L2Cache::LINE_SIZE> line{};
start.arrive_and_wait();
for (uint32_t i = 0; i < kLinesPerThread; ++i) {
const uint64_t addr =
kBase + (static_cast<uint64_t>(i) * kThreads + tid) * L2Cache::LINE_SIZE;
for (uint32_t b = 0; b < line.size(); ++b)
line[b] = static_cast<uint8_t>((tid << 4) ^ i ^ b);
l2.write(addr, line.data(), line.size());
}
});
}
for (auto &worker : workers)
worker.join();
std::array<uint8_t, L2Cache::LINE_SIZE> expected{};
std::array<uint8_t, L2Cache::LINE_SIZE> actual{};
for (uint32_t tid = 0; tid < kThreads; ++tid) {
for (uint32_t i = 0; i < kLinesPerThread; ++i) {
const uint64_t addr =
kBase + (static_cast<uint64_t>(i) * kThreads + tid) * L2Cache::LINE_SIZE;
for (uint32_t b = 0; b < expected.size(); ++b)
expected[b] = static_cast<uint8_t>((tid << 4) ^ i ^ b);
l2.read(addr, actual.data(), actual.size());
EXPECT_EQ(actual, expected) << "addr=0x" << std::hex << addr;
}
}
}
TEST(L2CacheThreadingTest, ConcurrentSameSetAccessesPreserveLines) {
GpuMemory memory("memory");
L2Cache l2("l2");
l2.set_backing_memory(&memory);
constexpr uint32_t kThreads = 8;
constexpr uint32_t kIterations = 256;
constexpr uint64_t kBase = 0x180000;
constexpr uint64_t kSetStride = static_cast<uint64_t>(L2Cache::NUM_SETS) * L2Cache::LINE_SIZE;
std::barrier start(kThreads);
std::atomic<uint64_t> mismatches{0};
std::vector<std::thread> workers;
workers.reserve(kThreads);
for (uint32_t tid = 0; tid < kThreads; ++tid) {
workers.emplace_back([&, tid] {
const uint64_t addr = kBase + tid * kSetStride;
std::array<uint8_t, L2Cache::LINE_SIZE> line{};
std::array<uint8_t, L2Cache::LINE_SIZE> actual{};
start.arrive_and_wait();
for (uint32_t iteration = 0; iteration < kIterations; ++iteration) {
line.fill(static_cast<uint8_t>((tid << 4) ^ iteration));
l2.write(addr, line.data(), line.size());
l2.read(addr, actual.data(), actual.size());
if (actual != line)
mismatches.fetch_add(1, std::memory_order_relaxed);
}
});
}
for (auto &worker : workers)
worker.join();
EXPECT_EQ(mismatches.load(std::memory_order_relaxed), 0u);
}
TEST(L2CacheThreadingTest, ConcurrentAtomicRmwSameLineIsSerialized) {
GpuMemory memory("memory");
L2Cache l2("l2");
l2.set_backing_memory(&memory);
constexpr uint32_t kThreads = 8;
constexpr uint32_t kIterations = 1000;
constexpr uint64_t kTarget = 0x200000;
memory.write32(kTarget, 0);
std::barrier start(kThreads);
std::vector<std::thread> workers;
workers.reserve(kThreads);
for (uint32_t tid = 0; tid < kThreads; ++tid) {
workers.emplace_back([&] {
start.arrive_and_wait();
for (uint32_t i = 0; i < kIterations; ++i) {
l2.atomic_rmw(kTarget, sizeof(uint32_t), [](uint8_t *line, uint32_t offset) {
uint32_t value = 0;
std::memcpy(&value, line + offset, sizeof(value));
++value;
std::memcpy(line + offset, &value, sizeof(value));
});
}
});
}
for (auto &worker : workers)
worker.join();
EXPECT_EQ(memory.read32(kTarget), kThreads * kIterations);
}
TEST(L2CacheThreadingTest, CrossL2AtomicRmwSameAddressIsSerialized) {
GpuMemory memory("memory");
L2Cache l2a("l2a");
L2Cache l2b("l2b");
l2a.set_backing_memory(&memory);
l2b.set_backing_memory(&memory);
constexpr uint32_t kThreads = 8;
constexpr uint32_t kIterations = 1000;
constexpr uint64_t kTarget = 0x280000;
memory.write32(kTarget, 0);
std::array<L2Cache *, 2> l2s = {&l2a, &l2b};
std::barrier start(kThreads);
std::vector<std::thread> workers;
workers.reserve(kThreads);
for (uint32_t tid = 0; tid < kThreads; ++tid) {
workers.emplace_back([&, tid] {
auto *l2 = l2s[tid % l2s.size()];
start.arrive_and_wait();
for (uint32_t i = 0; i < kIterations; ++i) {
l2->atomic_rmw(kTarget, sizeof(uint32_t), [](uint8_t *line, uint32_t offset) {
uint32_t value = 0;
std::memcpy(&value, line + offset, sizeof(value));
++value;
std::memcpy(line + offset, &value, sizeof(value));
});
}
});
}
for (auto &worker : workers)
worker.join();
EXPECT_EQ(memory.read32(kTarget), kThreads * kIterations);
}
TEST(L2CacheThreadingTest, CrossL2AtomicRmwAliasedVasIsSerialized) {
GpuMemory memory("memory");
L2Cache l2a("l2a");
L2Cache l2b("l2b");
l2a.set_backing_memory(&memory);
l2b.set_backing_memory(&memory);
constexpr uint32_t kVmidA = 7;
constexpr uint32_t kVmidB = 8;
constexpr uint64_t kVaA = 0x100000;
constexpr uint64_t kVaB = 0x201000;
constexpr uint64_t kOffset = 64;
constexpr uint32_t kThreads = 8;
constexpr uint32_t kIterations = 1000;
rocjitsu::KfdProcess process_a(kVmidA);
rocjitsu::KfdProcess process_b(kVmidB);
std::array<uint8_t, GpuMemory::PAGE_SIZE> backing{};
process_a.map_pages(kVaA, backing.data(), backing.size());
process_b.map_pages(kVaB, backing.data(), backing.size());
memory.register_process(kVmidA, &process_a.page_table_, &process_a.page_table_mutex_,
process_a.page_table_generation());
memory.register_process(kVmidB, &process_b.page_table_, &process_b.page_table_mutex_,
process_b.page_table_generation());
std::barrier start(kThreads);
std::vector<std::thread> workers;
workers.reserve(kThreads);
for (uint32_t tid = 0; tid < kThreads; ++tid) {
workers.emplace_back([&, tid] {
L2Cache &l2 = (tid & 1) ? l2b : l2a;
const uint64_t addr = ((tid & 1) ? kVaB : kVaA) + kOffset;
const uint32_t vmid = (tid & 1) ? kVmidB : kVmidA;
start.arrive_and_wait();
for (uint32_t i = 0; i < kIterations; ++i) {
l2.atomic_rmw(
addr, sizeof(uint32_t),
[](uint8_t *line, uint32_t offset) {
uint32_t value = 0;
std::memcpy(&value, line + offset, sizeof(value));
++value;
std::memcpy(line + offset, &value, sizeof(value));
},
vmid);
}
});
}
for (auto &worker : workers)
worker.join();
uint32_t actual = 0;
std::memcpy(&actual, backing.data() + kOffset, sizeof(actual));
EXPECT_EQ(actual, kThreads * kIterations);
}
TEST(L2CacheThreadingTest, CrossL2AtomicRmwDistinctSharedMappingsIncludesDirtyWriteback) {
constexpr size_t kMappingSize = GpuMemory::PAGE_SIZE;
const int raw_fd = static_cast<int>(syscall(SYS_memfd_create, "l2_atomic_alias", MFD_CLOEXEC));
ASSERT_GE(raw_fd, 0);
ScopedFd fd(raw_fd);
ASSERT_EQ(ftruncate(fd.get(), static_cast<off_t>(kMappingSize)), 0);
void *raw_mapping_a =
mmap(nullptr, kMappingSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0);
ASSERT_NE(raw_mapping_a, MAP_FAILED);
ScopedMapping mapping_a(raw_mapping_a, kMappingSize);
void *raw_mapping_b =
mmap(nullptr, kMappingSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0);
ASSERT_NE(raw_mapping_b, MAP_FAILED);
ScopedMapping mapping_b(raw_mapping_b, kMappingSize);
ASSERT_NE(mapping_a.data(), mapping_b.data());
constexpr uint32_t kVmidA = 17;
constexpr uint32_t kVmidB = 18;
constexpr uint64_t kVaA = 0x310000;
constexpr uint64_t kVaB = 0x420000;
constexpr uint32_t kOffset = 64;
rocjitsu::KfdProcess process_a(kVmidA);
rocjitsu::KfdProcess process_b(kVmidB);
process_a.map_pages(kVaA, mapping_a.data(), kMappingSize, Mtype::CC);
process_b.map_pages(kVaB, mapping_b.data(), kMappingSize, Mtype::CC);
GpuMemory memory("memory");
memory.register_process(kVmidA, &process_a.page_table_, &process_a.page_table_mutex_,
process_a.page_table_generation());
memory.register_process(kVmidB, &process_b.page_table_, &process_b.page_table_mutex_,
process_b.page_table_generation());
L2Cache l2a("l2a");
L2Cache l2b("l2b");
l2a.set_backing_memory(&memory);
l2b.set_backing_memory(&memory);
std::array<uint8_t, L2Cache::LINE_SIZE> dirty_line{};
constexpr uint32_t kDirtyValue = 40;
std::memcpy(dirty_line.data() + kOffset, &kDirtyValue, sizeof(kDirtyValue));
l2a.writeback_line(kVaA, dirty_line.data(), Mtype::RW, kVmidA);
auto increment = [](uint8_t *line, uint32_t offset) {
uint32_t value = 0;
std::memcpy(&value, line + offset, sizeof(value));
++value;
std::memcpy(line + offset, &value, sizeof(value));
};
// Both virtual addresses resolve to the same MAP_SHARED bytes. The first
// atomic boundary to enter must publish l2a's dirty line before either RMW,
// and both RMWs must remain serialized across the distinct L2 objects.
std::barrier start(3);
std::thread first_atomic([&] {
start.arrive_and_wait();
l2a.atomic_rmw(kVaA + kOffset, sizeof(uint32_t), increment, kVmidA);
});
std::thread second_atomic([&] {
start.arrive_and_wait();
l2b.atomic_rmw(kVaB + kOffset, sizeof(uint32_t), increment, kVmidB);
});
start.arrive_and_wait();
first_atomic.join();
second_atomic.join();
uint32_t actual = 0;
std::memcpy(&actual, mapping_a.data() + kOffset, sizeof(actual));
EXPECT_EQ(actual, 42u);
}
TEST(DeviceCacheCoherenceTest, ScalarWriteThroughSurvivesAliasedRemoteAtomic) {
constexpr size_t kMappingSize = GpuMemory::PAGE_SIZE;
const int raw_fd =
static_cast<int>(syscall(SYS_memfd_create, "scalar_atomic_alias", MFD_CLOEXEC));
ASSERT_GE(raw_fd, 0);
ScopedFd fd(raw_fd);
ASSERT_EQ(ftruncate(fd.get(), static_cast<off_t>(kMappingSize)), 0);
void *raw_scalar_mapping =
mmap(nullptr, kMappingSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0);
ASSERT_NE(raw_scalar_mapping, MAP_FAILED);
ScopedMapping scalar_mapping(raw_scalar_mapping, kMappingSize);
void *raw_atomic_mapping =
mmap(nullptr, kMappingSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0);
ASSERT_NE(raw_atomic_mapping, MAP_FAILED);
ScopedMapping atomic_mapping(raw_atomic_mapping, kMappingSize);
ASSERT_NE(scalar_mapping.data(), atomic_mapping.data());
constexpr uint32_t kScalarVmid = 21;
constexpr uint32_t kAtomicVmid = 22;
constexpr uint64_t kScalarVa = 0x510000;
constexpr uint64_t kAtomicVa = 0x620000;
constexpr uint64_t kLineOffset = L2Cache::LINE_SIZE;
constexpr uint64_t kAtomicAddr = kAtomicVa + kLineOffset;
constexpr uint64_t kScalarAddr = kScalarVa + kLineOffset + sizeof(uint32_t);
constexpr uint32_t kScalarValue = 0xA5A5A5A5;
rocjitsu::KfdProcess scalar_process(kScalarVmid);
rocjitsu::KfdProcess atomic_process(kAtomicVmid);
scalar_process.map_pages(kScalarVa, scalar_mapping.data(), kMappingSize, Mtype::RW);
atomic_process.map_pages(kAtomicVa, atomic_mapping.data(), kMappingSize, Mtype::RW);
GpuMemory memory("memory");
memory.register_process(kScalarVmid, &scalar_process.page_table_,
&scalar_process.page_table_mutex_,
scalar_process.page_table_generation());
memory.register_process(kAtomicVmid, &atomic_process.page_table_,
&atomic_process.page_table_mutex_,
atomic_process.page_table_generation());
L2Cache scalar_l2("scalar_l2");
L2Cache atomic_l2("atomic_l2");
rocjitsu::amdgpu::L1ScalarCache scalar_l1(&scalar_l2);
scalar_l2.set_backing_memory(&memory);
atomic_l2.set_backing_memory(&memory);
scalar_l1.store(kScalarAddr, /*num_dwords=*/1, &kScalarValue, kScalarVmid);
atomic_l2.atomic_rmw(kAtomicAddr, sizeof(uint32_t), increment_u32, kAtomicVmid);
scalar_l1.writeback_all(kScalarVmid);
uint32_t atomic_value = 0;
uint32_t scalar_value = 0;
std::memcpy(&atomic_value, scalar_mapping.data() + kLineOffset, sizeof(atomic_value));
std::memcpy(&scalar_value, scalar_mapping.data() + kLineOffset + sizeof(uint32_t),
sizeof(scalar_value));
EXPECT_EQ(atomic_value, 1u);
EXPECT_EQ(scalar_value, kScalarValue);
}
TEST(DeviceCacheCoherenceTest, DisjointDirtyL2AliasesSurviveRemoteAtomic) {
constexpr size_t kMappingSize = GpuMemory::PAGE_SIZE;
const int raw_fd =
static_cast<int>(syscall(SYS_memfd_create, "l2_dirty_atomic_alias", MFD_CLOEXEC));
ASSERT_GE(raw_fd, 0);
ScopedFd fd(raw_fd);
ASSERT_EQ(ftruncate(fd.get(), static_cast<off_t>(kMappingSize)), 0);
void *raw_mapping_a =
mmap(nullptr, kMappingSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0);
ASSERT_NE(raw_mapping_a, MAP_FAILED);
ScopedMapping mapping_a(raw_mapping_a, kMappingSize);
void *raw_mapping_b =
mmap(nullptr, kMappingSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0);
ASSERT_NE(raw_mapping_b, MAP_FAILED);
ScopedMapping mapping_b(raw_mapping_b, kMappingSize);
void *raw_mapping_atomic =
mmap(nullptr, kMappingSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd.get(), 0);
ASSERT_NE(raw_mapping_atomic, MAP_FAILED);
ScopedMapping mapping_atomic(raw_mapping_atomic, kMappingSize);
ASSERT_NE(mapping_a.data(), mapping_b.data());
ASSERT_NE(mapping_a.data(), mapping_atomic.data());
ASSERT_NE(mapping_b.data(), mapping_atomic.data());
constexpr uint32_t kVmidA = 31;
constexpr uint32_t kVmidB = 32;
constexpr uint32_t kAtomicVmid = 33;
constexpr uint64_t kVaA = 0x710000;
constexpr uint64_t kVaB = 0x820000;
constexpr uint64_t kAtomicVa = 0x930000;
constexpr uint64_t kLineOffset = L2Cache::LINE_SIZE;
constexpr uint32_t kValueA = 0x11112222;
constexpr uint32_t kValueB = 0x33334444;
constexpr uint32_t kOffsetA = sizeof(uint32_t);
constexpr uint32_t kOffsetB = 2 * sizeof(uint32_t);
rocjitsu::KfdProcess process_a(kVmidA);
rocjitsu::KfdProcess process_b(kVmidB);
rocjitsu::KfdProcess atomic_process(kAtomicVmid);
process_a.map_pages(kVaA, mapping_a.data(), kMappingSize, Mtype::RW);
process_b.map_pages(kVaB, mapping_b.data(), kMappingSize, Mtype::RW);
atomic_process.map_pages(kAtomicVa, mapping_atomic.data(), kMappingSize, Mtype::RW);
GpuMemory memory("memory");
memory.register_process(kVmidA, &process_a.page_table_, &process_a.page_table_mutex_,
process_a.page_table_generation());
memory.register_process(kVmidB, &process_b.page_table_, &process_b.page_table_mutex_,
process_b.page_table_generation());
memory.register_process(kAtomicVmid, &atomic_process.page_table_,
&atomic_process.page_table_mutex_,
atomic_process.page_table_generation());
L2Cache l2a("l2a");
L2Cache l2b("l2b");
L2Cache atomic_l2("atomic_l2");
l2a.set_backing_memory(&memory);
l2b.set_backing_memory(&memory);
atomic_l2.set_backing_memory(&memory);
std::array<uint8_t, L2Cache::LINE_SIZE> line_a{};
std::array<uint8_t, L2Cache::LINE_SIZE> line_b{};
std::memcpy(line_a.data() + kOffsetA, &kValueA, sizeof(kValueA));
std::memcpy(line_b.data() + kOffsetB, &kValueB, sizeof(kValueB));
l2a.writeback_line(kVaA + kLineOffset, line_a.data(), kOffsetA, sizeof(kValueA), Mtype::RW,
kVmidA);
l2b.writeback_line(kVaB + kLineOffset, line_b.data(), kOffsetB, sizeof(kValueB), Mtype::RW,
kVmidB);
atomic_l2.atomic_rmw(kAtomicVa + kLineOffset, sizeof(uint32_t), increment_u32, kAtomicVmid);
uint32_t atomic_value = 0;
uint32_t value_a = 0;
uint32_t value_b = 0;
std::memcpy(&atomic_value, mapping_a.data() + kLineOffset, sizeof(atomic_value));
std::memcpy(&value_a, mapping_a.data() + kLineOffset + kOffsetA, sizeof(value_a));
std::memcpy(&value_b, mapping_a.data() + kLineOffset + kOffsetB, sizeof(value_b));
EXPECT_EQ(atomic_value, 1u);
EXPECT_EQ(value_a, kValueA);
EXPECT_EQ(value_b, kValueB);
}
TEST(DeviceCacheCoherenceTest, ConcurrentScalarLoadsTrackRepeatedAtomicEpochs) {
GpuMemory memory("memory");
L2Cache scalar_l2("scalar_l2");
L2Cache atomic_l2("atomic_l2");
rocjitsu::amdgpu::L1ScalarCache scalar_l1(&scalar_l2);
scalar_l2.set_backing_memory(&memory);
atomic_l2.set_backing_memory(&memory);
constexpr uint64_t kAddr = 0xA600;
constexpr uint32_t kAtomicIterations = 256;
constexpr uint32_t kLoadIterations = 2048;
memory.write32(kAddr, 0);
uint32_t initially_cached = 1;
scalar_l1.load(kAddr, /*num_dwords=*/1, &initially_cached);
ASSERT_EQ(initially_cached, 0u);
std::mutex start_mutex;
std::condition_variable start_cv;
bool atomic_ready = false;
bool scalar_ready = false;
std::atomic<bool> synchronization_failed{false};
std::atomic<bool> nonmonotonic_load{false};
auto wait_for_peer = [&](bool &self_ready, const bool &peer_ready) {
std::unique_lock lock(start_mutex);
self_ready = true;
start_cv.notify_all();
if (!start_cv.wait_for(lock, std::chrono::seconds(1), [&] { return peer_ready; }))
synchronization_failed.store(true, std::memory_order_relaxed);
};
std::thread atomic([&] {
wait_for_peer(atomic_ready, scalar_ready);
for (uint32_t iteration = 0; iteration < kAtomicIterations; ++iteration) {
atomic_l2.atomic_rmw(kAddr, sizeof(uint32_t), increment_u32);
std::this_thread::yield();
}
});
std::thread scalar([&] {
wait_for_peer(scalar_ready, atomic_ready);
uint32_t previous = 0;
for (uint32_t iteration = 0; iteration < kLoadIterations; ++iteration) {
uint32_t observed = 0;
scalar_l1.load(kAddr, /*num_dwords=*/1, &observed);
if (observed < previous || observed > kAtomicIterations)
nonmonotonic_load.store(true, std::memory_order_relaxed);
previous = observed;
std::this_thread::yield();
}
});
atomic.join();
scalar.join();
EXPECT_FALSE(synchronization_failed.load(std::memory_order_relaxed));
EXPECT_FALSE(nonmonotonic_load.load(std::memory_order_relaxed));
uint32_t final_value = 0;
scalar_l1.load(kAddr, /*num_dwords=*/1, &final_value);
EXPECT_EQ(final_value, kAtomicIterations);
}
TEST(L2CacheTest, FunctionalLinkedPortAtomicRmwUpdatesBacking) {
constexpr uint64_t kBase = 0xB00000;
constexpr uint64_t kAddr = kBase + 20;
L2Cache l2("l2");
FunctionalMemoryPort backing("backing", kBase, 2 * L2Cache::LINE_SIZE);
simdojo::Link link(/*id=*/0, l2.req_port(), backing.port(), /*latency=*/0);
link.set_exec_mode(simdojo::ExecMode::FUNCTIONAL);
l2.req_port()->set_link(&link);
backing.port()->set_link(&link);
backing.write8(kAddr - 1, 0xA5);
backing.write32(kAddr, 41);
backing.write8(kAddr + sizeof(uint32_t), 0x5A);
uint32_t callback_old_value = 0;
l2.atomic_rmw(kAddr, sizeof(uint32_t), [&](uint8_t *target, uint32_t offset) {
EXPECT_EQ(offset, 0u);
std::memcpy(&callback_old_value, target, sizeof(callback_old_value));
const uint32_t replacement = callback_old_value + 1;
std::memcpy(target, &replacement, sizeof(replacement));
});
EXPECT_EQ(callback_old_value, 41u);
EXPECT_EQ(backing.read32(kAddr), 42u);
EXPECT_EQ(backing.read8(kAddr - 1), 0xA5);
EXPECT_EQ(backing.read8(kAddr + sizeof(uint32_t)), 0x5A);
}
TEST(L2CacheTest, LinkedPortAtomicRmwRefreshesStaleMemorySideAlias) {
constexpr uint32_t kVmidA = 41;
constexpr uint32_t kVmidB = 42;
constexpr uint64_t kVaA = 0xC10000;
constexpr uint64_t kVaB = 0xD20000;
constexpr uint32_t kOffset = 64;
constexpr uint32_t kPublishedValue = 40;
rocjitsu::KfdProcess process_a(kVmidA);
rocjitsu::KfdProcess process_b(kVmidB);
std::array<uint8_t, GpuMemory::PAGE_SIZE> backing{};
process_a.map_pages(kVaA, backing.data(), backing.size(), Mtype::RW);
process_b.map_pages(kVaB, backing.data(), backing.size(), Mtype::RW);
GpuMemory memory("memory");
memory.register_process(kVmidA, &process_a.page_table_, &process_a.page_table_mutex_,
process_a.page_table_generation());
memory.register_process(kVmidB, &process_b.page_table_, &process_b.page_table_mutex_,
process_b.page_table_generation());
HbmController hbm("hbm", &memory);
MemorySideCache msc("msc");
L2Cache l2a("l2a");
L2Cache l2b("l2b");
simdojo::Port *l2a_msc_port = msc.create_cpl_port("l2a");
simdojo::Port *l2b_msc_port = msc.create_cpl_port("l2b");
simdojo::Link msc_hbm_link(/*id=*/0, msc.req_port(), hbm.cpl_port(), /*latency=*/0);
simdojo::Link l2a_msc_link(/*id=*/1, l2a.req_port(), l2a_msc_port, /*latency=*/0);
simdojo::Link l2b_msc_link(/*id=*/2, l2b.req_port(), l2b_msc_port, /*latency=*/0);
for (simdojo::Link *link : {&msc_hbm_link, &l2a_msc_link, &l2b_msc_link})
link->set_exec_mode(simdojo::ExecMode::FUNCTIONAL);
msc.req_port()->set_link(&msc_hbm_link);
hbm.cpl_port()->set_link(&msc_hbm_link);
l2a.req_port()->set_link(&l2a_msc_link);
l2a_msc_port->set_link(&l2a_msc_link);
l2b.req_port()->set_link(&l2b_msc_link);
l2b_msc_port->set_link(&l2b_msc_link);
uint32_t stale_value = 1;
l2b.read(kVaB + kOffset, reinterpret_cast<uint8_t *>(&stale_value), sizeof(stale_value),
Mtype::RW, kVmidB);
ASSERT_EQ(stale_value, 0u);
std::array<uint8_t, L2Cache::LINE_SIZE> dirty_line{};
std::memcpy(dirty_line.data() + kOffset, &kPublishedValue, sizeof(kPublishedValue));
l2a.writeback_line(kVaA, dirty_line.data(), kOffset, sizeof(kPublishedValue), Mtype::RW, kVmidA);
uint32_t callback_old_value = 0;
l2b.atomic_rmw(
kVaB + kOffset, sizeof(uint32_t),
[&](uint8_t *target, uint32_t offset) {
std::memcpy(&callback_old_value, target + offset, sizeof(callback_old_value));
const uint32_t replacement = callback_old_value + 1;
std::memcpy(target + offset, &replacement, sizeof(replacement));
},
kVmidB);
EXPECT_EQ(callback_old_value, kPublishedValue);
uint32_t host_value = 0;
std::memcpy(&host_value, backing.data() + kOffset, sizeof(host_value));
EXPECT_EQ(host_value, kPublishedValue + 1);
uint32_t alias_a_value = 0;
uint32_t alias_b_value = 0;
l2a.read(kVaA + kOffset, reinterpret_cast<uint8_t *>(&alias_a_value), sizeof(alias_a_value),
Mtype::RW, kVmidA);
l2b.read(kVaB + kOffset, reinterpret_cast<uint8_t *>(&alias_b_value), sizeof(alias_b_value),
Mtype::RW, kVmidB);
EXPECT_EQ(alias_a_value, kPublishedValue + 1);
EXPECT_EQ(alias_b_value, kPublishedValue + 1);
}
TEST(L2CacheTest, AliasedVasRequireCoherenceBoundary) {
GpuMemory memory("memory");
L2Cache l2("l2");
l2.set_backing_memory(&memory);
constexpr uint32_t kVmidA = 7;
constexpr uint32_t kVmidB = 8;
constexpr uint64_t kVaA = 0x100000;
constexpr uint64_t kVaB = 0x201000;
rocjitsu::KfdProcess process_a(kVmidA);
rocjitsu::KfdProcess process_b(kVmidB);
std::array<uint8_t, GpuMemory::PAGE_SIZE> backing{};
process_a.map_pages(kVaA, backing.data(), backing.size());
process_b.map_pages(kVaB, backing.data(), backing.size());
memory.register_process(kVmidA, &process_a.page_table_, &process_a.page_table_mutex_,
process_a.page_table_generation());
memory.register_process(kVmidB, &process_b.page_table_, &process_b.page_table_mutex_,
process_b.page_table_generation());
std::array<uint8_t, L2Cache::LINE_SIZE> initial{};
std::array<uint8_t, L2Cache::LINE_SIZE> replacement{};
std::array<uint8_t, L2Cache::LINE_SIZE> dirty{};
std::array<uint8_t, L2Cache::LINE_SIZE> actual{};
initial.fill(0x11);
replacement.fill(0x22);
dirty.fill(0x33);
memory.write_block(kVaA, std::span<const uint8_t>(initial), kVmidA);
l2.read(kVaA, actual.data(), actual.size(), Mtype::RW, kVmidA);
ASSERT_EQ(actual, initial);
l2.read(kVaB, actual.data(), actual.size(), Mtype::RW, kVmidB);
ASSERT_EQ(actual, initial);
l2.write(kVaB, replacement.data(), replacement.size(), Mtype::RW, kVmidB);
l2.read(kVaA, actual.data(), actual.size(), Mtype::RW, kVmidA);
EXPECT_EQ(actual, initial);
l2.read(kVaA, actual.data(), actual.size(), Mtype::CC, kVmidA);
EXPECT_EQ(actual, replacement);
l2.writeback_line(kVaA, dirty.data(), Mtype::RW, kVmidA);
memory.read_block(kVaB, std::span<uint8_t>(actual), kVmidB);
EXPECT_EQ(actual, replacement);
l2.flush_line(kVaA, kVmidA);
memory.read_block(kVaB, std::span<uint8_t>(actual), kVmidB);
EXPECT_EQ(actual, dirty);
l2.read(kVaB, actual.data(), actual.size(), Mtype::RW, kVmidB);
EXPECT_EQ(actual, replacement);
l2.read(kVaB, actual.data(), actual.size(), Mtype::CC, kVmidB);
EXPECT_EQ(actual, dirty);
}
TEST(L2CacheThreadingTest, ConcurrentFlushAllPreservesDirtyWritebacks) {
GpuMemory memory("memory");
L2Cache l2("l2");
l2.set_backing_memory(&memory);
constexpr uint32_t kWriterThreads = 4;
constexpr uint32_t kLinesPerThread = 8;
constexpr uint32_t kIterations = 64;
constexpr uint64_t kBase = 0x300000;
std::atomic<uint32_t> active_writers{0};
std::barrier start(kWriterThreads + 1);
std::vector<std::thread> workers;
workers.reserve(kWriterThreads);
for (uint32_t tid = 0; tid < kWriterThreads; ++tid) {
workers.emplace_back([&, tid] {
std::array<uint8_t, L2Cache::LINE_SIZE> line{};
start.arrive_and_wait();
active_writers.fetch_add(1, std::memory_order_release);
for (uint32_t iteration = 0; iteration < kIterations; ++iteration) {
for (uint32_t i = 0; i < kLinesPerThread; ++i) {
const uint64_t addr =
kBase + (static_cast<uint64_t>(i) * kWriterThreads + tid) * L2Cache::LINE_SIZE;
for (uint32_t b = 0; b < line.size(); ++b)
line[b] = static_cast<uint8_t>((tid << 5) ^ iteration ^ i ^ b);
l2.writeback_line(addr, line.data());
}
std::this_thread::yield();
}
});
}
std::thread flusher([&] {
start.arrive_and_wait();
while (active_writers.load(std::memory_order_acquire) < kWriterThreads)
std::this_thread::yield();
for (uint32_t i = 0; i < 4; ++i) {
l2.flush_all();
std::this_thread::yield();
}
});
for (auto &worker : workers)
worker.join();
flusher.join();
l2.flush_all();
std::array<uint8_t, L2Cache::LINE_SIZE> expected{};
std::array<uint8_t, L2Cache::LINE_SIZE> actual{};
for (uint32_t tid = 0; tid < kWriterThreads; ++tid) {
for (uint32_t i = 0; i < kLinesPerThread; ++i) {
const uint64_t addr =
kBase + (static_cast<uint64_t>(i) * kWriterThreads + tid) * L2Cache::LINE_SIZE;
for (uint32_t b = 0; b < expected.size(); ++b)
expected[b] = static_cast<uint8_t>((tid << 5) ^ (kIterations - 1) ^ i ^ b);
memory.read_block(addr, std::span<uint8_t>(actual));
EXPECT_EQ(actual, expected) << "addr=0x" << std::hex << addr;
}
}
}
TEST(L2CacheTest, InvalidateRangeClampsAtAddressSpaceEnd) {
GpuMemory memory("memory");
L2Cache l2("l2");
l2.set_backing_memory(&memory);
constexpr uint64_t kLineAddr = std::numeric_limits<uint64_t>::max() - (L2Cache::LINE_SIZE - 1);
constexpr uint64_t kRangeAddr =
std::numeric_limits<uint64_t>::max() - (L2Cache::LINE_SIZE / 2 - 1);
std::array<uint8_t, L2Cache::LINE_SIZE> initial{};
std::array<uint8_t, L2Cache::LINE_SIZE> replacement{};
std::array<uint8_t, L2Cache::LINE_SIZE> actual{};
initial.fill(0x11);
replacement.fill(0x22);
memory.write_block(kLineAddr, std::span<const uint8_t>(initial));
l2.read(kLineAddr, actual.data(), actual.size());
ASSERT_EQ(actual, initial);
memory.write_block(kLineAddr, std::span<const uint8_t>(replacement));
l2.invalidate_range(kRangeAddr, L2Cache::LINE_SIZE, 0);
l2.read(kLineAddr, actual.data(), actual.size());
EXPECT_EQ(actual, replacement);
}
TEST(L2CacheTest, InvalidateRangeRefreshesOnlyCoveredSetsAndZeroSizeIsNoOp) {
GpuMemory memory("memory");
L2Cache l2("l2");
l2.set_backing_memory(&memory);
constexpr uint64_t kBase = 0x400000;
constexpr uint32_t kLines = 5;
std::array<std::array<uint8_t, L2Cache::LINE_SIZE>, kLines> initial{};
std::array<std::array<uint8_t, L2Cache::LINE_SIZE>, kLines> replacement{};
std::array<uint8_t, L2Cache::LINE_SIZE> actual{};
for (uint32_t line = 0; line < kLines; ++line) {
initial[line].fill(static_cast<uint8_t>(0x10 + line));
replacement[line].fill(static_cast<uint8_t>(0x80 + line));
const uint64_t addr = kBase + static_cast<uint64_t>(line) * L2Cache::LINE_SIZE;
memory.write_block(addr, std::span<const uint8_t>(initial[line]));
l2.read(addr, actual.data(), actual.size());
ASSERT_EQ(actual, initial[line]);
memory.write_block(addr, std::span<const uint8_t>(replacement[line]));
}
l2.invalidate_range(kBase + L2Cache::LINE_SIZE + 32, 2 * L2Cache::LINE_SIZE, 0);
for (uint32_t line = 0; line < kLines; ++line) {
const uint64_t addr = kBase + static_cast<uint64_t>(line) * L2Cache::LINE_SIZE;
l2.read(addr, actual.data(), actual.size());
const auto &expected = (line >= 1 && line <= 3) ? replacement[line] : initial[line];
EXPECT_EQ(actual, expected) << "line=" << line;
}
l2.invalidate_range(kBase, 0, 0);
l2.read(kBase, actual.data(), actual.size());
EXPECT_EQ(actual, initial[0]);
}
TEST(L2CacheTest, InvalidateRangeOver128LinesUsesExclusiveMaintenance) {
GpuMemory memory("memory");
L2Cache l2("l2");
l2.set_backing_memory(&memory);
constexpr uint64_t kBase = 0x700000;
constexpr uint32_t kLines = 129;
std::array<uint8_t, L2Cache::LINE_SIZE> initial{};
std::array<uint8_t, L2Cache::LINE_SIZE> replacement{};
std::array<uint8_t, L2Cache::LINE_SIZE> actual{};
for (uint32_t line = 0; line < kLines; ++line) {
const uint64_t addr = kBase + static_cast<uint64_t>(line) * L2Cache::LINE_SIZE;
initial.fill(static_cast<uint8_t>(line));
replacement.fill(static_cast<uint8_t>(line + 0x40));
memory.write_block(addr, std::span<const uint8_t>(initial));
l2.read(addr, actual.data(), actual.size());
ASSERT_EQ(actual, initial) << "line=" << line;
memory.write_block(addr, std::span<const uint8_t>(replacement));
}
l2.invalidate_range(kBase, kLines * L2Cache::LINE_SIZE, 0);
for (uint32_t line = 0; line < kLines; ++line) {
const uint64_t addr = kBase + static_cast<uint64_t>(line) * L2Cache::LINE_SIZE;
replacement.fill(static_cast<uint8_t>(line + 0x40));
l2.read(addr, actual.data(), actual.size());
EXPECT_EQ(actual, replacement) << "line=" << line;
}
}
TEST(L2CacheTest, InvalidateRangeOnlyAffectsRequestedVmid) {
GpuMemory memory("memory");
L2Cache l2("l2");
l2.set_backing_memory(&memory);
constexpr uint32_t kVmidA = 7;
constexpr uint32_t kVmidB = 8;
constexpr uint64_t kAddr = 0x500000;
rocjitsu::KfdProcess process_a(kVmidA);
rocjitsu::KfdProcess process_b(kVmidB);
std::array<uint8_t, GpuMemory::PAGE_SIZE> backing_a{};
std::array<uint8_t, GpuMemory::PAGE_SIZE> backing_b{};
process_a.map_pages(kAddr, backing_a.data(), backing_a.size());
process_b.map_pages(kAddr, backing_b.data(), backing_b.size());
memory.register_process(kVmidA, &process_a.page_table_, &process_a.page_table_mutex_,
process_a.page_table_generation());
memory.register_process(kVmidB, &process_b.page_table_, &process_b.page_table_mutex_,
process_b.page_table_generation());
std::array<uint8_t, L2Cache::LINE_SIZE> initial_a{};
std::array<uint8_t, L2Cache::LINE_SIZE> initial_b{};
std::array<uint8_t, L2Cache::LINE_SIZE> dirty_a{};
std::array<uint8_t, L2Cache::LINE_SIZE> replacement_b{};
std::array<uint8_t, L2Cache::LINE_SIZE> actual{};
initial_a.fill(0x11);
initial_b.fill(0x22);
dirty_a.fill(0x33);
replacement_b.fill(0x44);
memory.write_block(kAddr, std::span<const uint8_t>(initial_a), kVmidA);
memory.write_block(kAddr, std::span<const uint8_t>(initial_b), kVmidB);
l2.read(kAddr, actual.data(), actual.size(), Mtype::RW, kVmidA);
ASSERT_EQ(actual, initial_a);
l2.read(kAddr, actual.data(), actual.size(), Mtype::RW, kVmidB);
ASSERT_EQ(actual, initial_b);
l2.writeback_line(kAddr, dirty_a.data(), Mtype::RW, kVmidA);
memory.write_block(kAddr, std::span<const uint8_t>(replacement_b), kVmidB);
l2.invalidate_range(kAddr, L2Cache::LINE_SIZE, kVmidB);
l2.read(kAddr, actual.data(), actual.size(), Mtype::RW, kVmidB);
EXPECT_EQ(actual, replacement_b);
l2.flush_line(kAddr, kVmidA);
memory.read_block(kAddr, std::span<uint8_t>(actual), kVmidA);
EXPECT_EQ(actual, dirty_a);
}
TEST(L2CacheTest, InvalidateRangePreservesDirtyBytesOutsidePartialWrite) {
GpuMemory memory("memory");
L2Cache l2("l2");
l2.set_backing_memory(&memory);
constexpr uint64_t kAddr = 0x600000;
constexpr uint32_t kOffset = 32;
std::array<uint8_t, L2Cache::LINE_SIZE> initial{};
std::array<uint8_t, L2Cache::LINE_SIZE> dirty{};
std::array<uint8_t, 16> host_write{};
std::array<uint8_t, L2Cache::LINE_SIZE> expected{};
std::array<uint8_t, L2Cache::LINE_SIZE> actual{};
initial.fill(0x11);
dirty.fill(0x22);
host_write.fill(0x33);
expected = dirty;
std::memcpy(expected.data() + kOffset, host_write.data(), host_write.size());
memory.write_block(kAddr, std::span<const uint8_t>(initial));
l2.read(kAddr, actual.data(), actual.size());
ASSERT_EQ(actual, initial);
l2.writeback_line(kAddr, dirty.data());
memory.write_block(kAddr + kOffset, std::span<const uint8_t>(host_write));
l2.invalidate_range(kAddr + kOffset, host_write.size(), 0);
memory.read_block(kAddr, std::span<uint8_t>(actual));
EXPECT_EQ(actual, expected);
l2.read(kAddr, actual.data(), actual.size());
EXPECT_EQ(actual, expected);
}
TEST(L2CacheBenchmark, CrossL2SameAddress) {
run_cross_l2_atomic_benchmark("cross_l2_same_address", true);
}
TEST(L2CacheBenchmark, CrossL2IndependentAddresses) {
run_cross_l2_atomic_benchmark("cross_l2_independent_addresses", false);
}
TEST(L2CacheBenchmark, AtomicOneHierarchy) {
run_atomic_hierarchy_benchmark("atomic_hierarchies_1", 1);
}
TEST(L2CacheBenchmark, AtomicTwoHierarchies) {
run_atomic_hierarchy_benchmark("atomic_hierarchies_2", 2);
}
TEST(L2CacheBenchmark, AtomicFourHierarchies) {
run_atomic_hierarchy_benchmark("atomic_hierarchies_4", 4);
}
TEST(L2CacheBenchmark, AtomicEightHierarchies) {
run_atomic_hierarchy_benchmark("atomic_hierarchies_8", 8);
}
TEST(L2CacheBenchmark, ScalarL1Hit) { run_scalar_l1_hit_benchmark(); }
TEST(L2CacheBenchmark, VectorL1Hit) { run_vector_l1_hit_benchmark(); }
TEST(L2CacheBenchmark, InvalidateThreeLines) {
run_invalidate_range_benchmark("invalidate_3_lines", 3, 200'000);
}
TEST(L2CacheBenchmark, Invalidate129Lines) {
run_invalidate_range_benchmark("invalidate_129_lines", 129, 5'000);
}
TEST(GpuMemoryTest, BlockAccessHandlesPageBoundaries) {
GpuMemory memory("memory");
constexpr uint64_t kAddr = 0x3ff0;
std::array<uint8_t, 64> input{};
std::array<uint8_t, 64> output{};
for (uint32_t i = 0; i < input.size(); ++i)
input[i] = static_cast<uint8_t>(i * 3);
memory.write_block(kAddr, std::span<const uint8_t>(input));
memory.read_block(kAddr, std::span<uint8_t>(output));
EXPECT_EQ(output, input);
}
} // namespace