Measure whether device-local memory takes process commit instead of assuming it per vendor

c838d96 counted system memory for every discrete GPU but NVIDIA's, an assumption. An RTX 5080 log (#398) showed 10.4 GB
of process commit at frame 1200, about what the RX 9070 holds with its VRAM counted, and the RTX 3060 Ti in #670 ran
out of system commit (17.8 GB) at 4K with its VRAM full. vk_mem_host_probe now allocates 128 MiB of device-local memory
once after device creation and compares the process commit before and after; half the size or more turns the system
memory cap on. The log line carries the measured amount, so user logs show what each driver does.

RX 9070: "a 128 MiB device-local allocation took 128 MiB of process commit".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valiantsin DzerakhandClaude Opus 5.5 committed 2026-11-09 09:39:54 +03:00
1 parent a689922180
commit 79c6951688
3 files changed
+49 -10

No files matched your search

+3 -1
View File
@@ -304,8 +304,10 @@ void vk_mem_census(uint64_t frame); // at each present: pool censu
// (vk_mem_host_room) the budget is at most the use plus that room.
int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact);
// Bytes the system can still give device memory beyond a reserve (TOTK_MEM_RESERVE_MIB) where device memory takes host
// memory: a UMA GPU (commit and physical memory), or a discrete non-NVIDIA GPU on Windows (commit). UINT64_MAX elsewhere.
// memory: a UMA GPU (commit and physical memory), or a discrete GPU whose device-local memory took process commit in
// vk_mem_host_probe (commit). UINT64_MAX elsewhere.
uint64_t vk_mem_host_room(void);
void vk_mem_host_probe(void); // vk_device.c, after the device: measures that for a discrete GPU, once
int vk_mem_uma(void); // 1: the device class is uma (vk_mem_class_log), its memory is RAM
// VRAM headroom (vk_headroom.c): the VMA callbacks that see new device memory, and the thread that allocates the next
// block of each memory type in use ahead of the render thread.
+2 -1
View File
@@ -475,7 +475,8 @@ int vk_init(void) {
plat_mutex_lock(&g_lock);
if (!g_ready) {
memset(&g_vk, 0, sizeof g_vk);
if (create_instance() && pick_device() && create_device()) g_ready = 1, vk_upload_pools_init(), vk_headroom_start(), vk_dlaa_init();
if (create_instance() && pick_device() && create_device())
g_ready = 1, vk_mem_host_probe(), vk_upload_pools_init(), vk_headroom_start(), vk_dlaa_init();
else destroy_all();
}
int ok = g_ready;
+44 -8
View File
@@ -589,6 +589,7 @@ void vk_mem_census(uint64_t frame) {
"%.0f of %.0f MiB", h, vc->mem_props.memoryHeaps[h].flags, (unsigned long long)frame,
vb[h].statistics.blockCount, vb[h].statistics.blockBytes / 1048576.0, vb[h].statistics.allocationCount,
vb[h].statistics.allocationBytes / 1048576.0, vb[h].usage / 1048576.0, vb[h].budget / 1048576.0);
}
}
static const char *const kinds[] = {"", "import", "physical", "virtual"};
for (int j = 0; j < ng; j++) {
@@ -624,10 +625,11 @@ static uint64_t vram_limit(void) {
// Device memory that takes host memory. A UMA GPU's memory is the machine's RAM. On Windows the AMD driver charges
// device-local allocations to the process commit one to one (RX 9070: VRAM use and private bytes grew by the same
// 1164, 256 and 256 MiB), so on a 16 GB machine the commit limit is reached while the VRAM budget shows gigabytes free,
// and the driver then fails inside a call (#285: std::bad_alloc in amdvlk64.dll under gpl_link). Intel's Windows driver
// is taken to do the same; NVIDIA's is not measured and left out. Linux overcommits, so there only a UMA GPU's physical
// memory counts (plat_mem_room).
enum { HB_NONE, HB_COMMIT, HB_UMA };
// and the driver then fails inside a call (#285: std::bad_alloc in amdvlk64.dll under gpl_link). Whether a discrete
// GPU's driver does so is measured once (vk_mem_host_probe), not assumed per vendor: an RTX 5080 log (#398) showed
// 10.4 GB of process commit at frame 1200, about what the RX 9070 holds with its VRAM counted. Linux overcommits, so
// there only a UMA GPU's physical memory counts (plat_mem_room).
enum { HB_NONE, HB_COMMIT, HB_UMA, HB_PROBE };
static int host_backed;
static uint64_t room_reserve;
static volatile uint64_t room_cached, room_ms;
@@ -647,6 +649,40 @@ uint64_t vk_mem_host_room(void) {
int vk_mem_uma(void) { return host_backed == HB_UMA; }
// A discrete GPU: one device-local allocation of HB_PROBE_SIZE, and the process commit before and after it. The driver
// charges it at allocation or not at all as far as this sees; one that charged only at first use would read as not
// counted, as before the probe. Another thread's allocation in between could only add, and half the size is the line.
#define HB_PROBE_SIZE (128ull << 20)
void vk_mem_host_probe(void) {
VkCtx *c = vk_ctx();
if (!c || host_backed != HB_PROBE) return;
host_backed = HB_NONE;
uint64_t commit, phys;
plat_mem_room(&commit, &phys);
if (commit == UINT64_MAX) return; // no commit limit (Linux)
uint32_t type = UINT32_MAX;
for (uint32_t i = 0; i < c->mem_props.memoryTypeCount && type == UINT32_MAX; i++)
if ((c->mem_props.memoryTypes[i].propertyFlags & (VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT)) ==
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)
type = i;
VkMemoryAllocateInfo ai = {.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .allocationSize = HB_PROBE_SIZE,
.memoryTypeIndex = type};
uint64_t r0, p0, r1, p1;
VkDeviceMemory m;
plat_process_memory(&r0, &p0);
if (type == UINT32_MAX || !p0 || vkAllocateMemory(c->device, &ai, NULL, &m) != VK_SUCCESS) {
rt_log("mem: device memory against system memory: not measured (no device-local type or allocation failed): not counted");
return;
}
plat_process_memory(&r1, &p1);
vkFreeMemory(c->device, m, NULL);
int64_t d = (int64_t)(p1 - p0);
host_backed = d >= (int64_t)HB_PROBE_SIZE / 2 ? HB_COMMIT : HB_NONE;
rt_log("mem: device memory against system memory: %s (a %llu MiB device-local allocation took %.0f MiB of process "
"commit), reserve %llu MiB (TOTK_MEM_RESERVE_MIB)", host_backed == HB_COMMIT ? "commit" : "not counted",
(unsigned long long)(HB_PROBE_SIZE >> 20), d / 1048576.0, (unsigned long long)(room_reserve >> 20));
}
// The budget is at most the use plus what the system can still give, so the paths that react to VRAM pressure (the
// mirror refusal, released textures, the headroom block) react to a system short of memory too.
static void host_cap(uint64_t used, uint64_t *budget) {
@@ -2285,7 +2321,7 @@ void vk_mem_class_log(const VkPhysicalDeviceProperties *p, const VkPhysicalDevic
else if (host_heap < 0) cls = "uma", why = "every heap is device-local";
else cls = "discrete", why = "device-local heap separate from host memory";
int discrete = cls[0] == 'd';
host_backed = !discrete ? HB_UMA : p->vendorID != 0x10DE ? HB_COMMIT : HB_NONE;
host_backed = !discrete ? HB_UMA : HB_PROBE; // a discrete GPU is measured once the device exists
const char *rs = cfg_get("TOTK_MEM_RESERVE_MIB");
room_reserve = (rs && *rs ? strtoull(rs, NULL, 10) : 1536) << 20; // Windows, other programs, our own heap
char line[2048];
@@ -2307,9 +2343,9 @@ void vk_mem_class_log(const VkPhysicalDeviceProperties *p, const VkPhysicalDevic
f & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT ? " LAZY" : "");
}
rt_log("%s", line);
rt_log("mem: device memory against system memory: %s, reserve %llu MiB (TOTK_MEM_RESERVE_MIB)",
host_backed == HB_UMA ? "commit and physical memory (uma)" : host_backed == HB_COMMIT ? "commit" : "not counted",
(unsigned long long)(room_reserve >> 20));
if (host_backed == HB_UMA)
rt_log("mem: device memory against system memory: commit and physical memory (uma), reserve %llu MiB "
"(TOTK_MEM_RESERVE_MIB)", (unsigned long long)(room_reserve >> 20));
mirror_mode(discrete);
}