diff --git a/runtime/vk.h b/runtime/vk.h index 93445bb6..9cac3e54 100644 --- a/runtime/vk.h +++ b/runtime/vk.h @@ -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. diff --git a/runtime/vk_device.c b/runtime/vk_device.c index 31fbc1a4..458cd86b 100644 --- a/runtime/vk_device.c +++ b/runtime/vk_device.c @@ -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; diff --git a/runtime/vk_memory.c b/runtime/vk_memory.c index 2fb547e7..b493b19d 100644 --- a/runtime/vk_memory.c +++ b/runtime/vk_memory.c @@ -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); }