diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e125642f5..9f766a367b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1720,3 +1720,23 @@ if(EXISTS "${_GGUF_CAPS_TEST_SRC}") include(CTest) add_test(NAME GgufCapabilitiesTest COMMAND test_gguf_capabilities) endif() + +# AMD VRAM unified memory detection (Strix Halo/Point GTT-aware calculation). +# Linux-only: KFD /sysfs path, pthread linkage. has_unified_memory() is header-only. +if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/cpp/test_amd_vram.cpp") + add_executable(test_amd_vram + test/cpp/test_amd_vram.cpp + ) + target_include_directories(test_amd_vram PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/cpp/include + ${CMAKE_CURRENT_BINARY_DIR}/include + ) + target_link_libraries(test_amd_vram PRIVATE nlohmann_json::nlohmann_json) + if(UNIX) + target_link_libraries(test_amd_vram PRIVATE pthread) + target_link_options(test_amd_vram PRIVATE -pthread) + endif() + + include(CTest) + add_test(NAME AmdVramTest COMMAND test_amd_vram) +endif() diff --git a/src/cpp/include/lemon/system_info.h b/src/cpp/include/lemon/system_info.h index 9b143ae47a..a511d61a61 100644 --- a/src/cpp/include/lemon/system_info.h +++ b/src/cpp/include/lemon/system_info.h @@ -48,6 +48,17 @@ enum class MemoryAllocBehavior Largest = 4, }; +// Unified memory detection for AMD APU (Strix Halo gfx1105, Strix Point gfx1102). +// Returns true if the GPU uses GTT as its primary memory pool. +// Header-only — used by get_global_vram_usage_pct() and testable without linking system_info.cpp. +// gfx_target_version from /sys/class/kfd/kfd/topology/nodes/*/properties/gfx_target_version +inline bool has_unified_memory(uint32_t gfx_target_version) { + // Strix Point: 1102xx → family 11, sub 02 + // Strix Halo: 1105xx → family 11, sub 05 + return gfx_target_version / 10000 == 11 && ((gfx_target_version % 10000) / 100 == 2 || (gfx_target_version % 10000) / 100 == 5); +} + + // Base class for system information class SystemInfo { public: @@ -130,6 +141,10 @@ class SystemInfo { // Global GPU memory pressure across all processes (used/total in [0,1]), // or -1.0 if no source is available. Used by the dynamic VRAM eviction engine. static double get_global_vram_usage_pct(); + + // Scan KFD for Strix APU (gfx1105, gfx1102) — returns true if a unified-memory GPU is found. + // Used by get_global_vram_usage_pct() to skip nvidia-smi on AMD systems. + static bool is_strix_apu(); }; // Windows implementation diff --git a/src/cpp/server/system_info.cpp b/src/cpp/server/system_info.cpp index d2d3f7d51e..9d3d29d6e3 100644 --- a/src/cpp/server/system_info.cpp +++ b/src/cpp/server/system_info.cpp @@ -4064,14 +4064,82 @@ bool SystemInfo::is_running_under_systemd() { #endif } +// Scan KFD topology for Strix APU (gfx1102, gfx1105). Returns true if a +// unified-memory GPU is detected, false otherwise or if KFD is unavailable. +bool SystemInfo::is_strix_apu() { + const std::string kfd_nodes = "/sys/class/kfd/kfd/topology/nodes"; + std::filesystem::path p(kfd_nodes); + if (!std::filesystem::exists(p)) { + return false; + } + for (const auto& node : std::filesystem::directory_iterator(p)) { + std::string props_path = node.path().string() + "/properties"; + std::ifstream props(props_path); + if (!props.is_open()) { + continue; + } + std::string line; + while (std::getline(props, line)) { + if (line.find("gfx_target_version") == 0) { + std::string val = line.substr(line.find(" ") + 1); + try { + uint32_t gfx = static_cast(std::stoul(val)); + if (has_unified_memory(gfx)) { + return true; + } + } catch (...) {} + } + } + } + return false; +} + double SystemInfo::get_global_vram_usage_pct() { // Report *global* GPU memory pressure (all processes, not just lemonade's), // so the eviction engine yields VRAM when other apps (ComfyUI, games, etc.) // consume it. Returns used/total in [0,1], or -1.0 if no source is available. // - // Reuses the same detection sources as the rest of this file: nvidia-smi for - // NVIDIA (Linux + Windows) and AMD sysfs for Linux. macOS/Metal is unsupported - // for now and falls through to -1.0. + // On NVIDIA: reads memory.used/memory.total from nvidia-smi. + // On AMD Strix Halo/Point (unified memory): reads (vram_used + gtt_used) / + // (vram_total + gtt_total) from sysfs, because GTT is where models actually + // live. On discrete AMD GPUs, falls back to vram_only. + // + // Early exit for pure AMD systems: skip nvidia-smi subprocess entirely to + // avoid stderr log spam and unnecessary fork overhead on systems without + // NVIDIA drivers. We check KFD (Linux-only) since its presence guarantees an + // AMD GPU; if no NVIDIA PCI device exists either, we know nvidia-smi is + // useless. + +#ifdef __linux__ + { + bool has_kfd = fs::exists("/sys/class/kfd"); + if (!has_kfd) goto try_nvidia; + + // AMD GPU confirmed via KFD. Check if an NVIDIA PCI device exists + // (10de is the NVIDIA vendor ID). If not, skip nvidia-smi entirely. + bool has_nvidia_gpu = false; + { + std::string pci_path = "/sys/bus/pci/devices"; + try { + if (fs::exists(pci_path)) { + for (const auto& dev : fs::directory_iterator(pci_path)) { + std::ifstream vendor(dev.path().string() + "/vendor"); + if (vendor.is_open()) { + std::string id; + std::getline(vendor, id); + if (id == "0x10de") { has_nvidia_gpu = true; break; } + } + } + } + } catch (...) {} + } + if (!has_nvidia_gpu) { + // Pure AMD system — skip nvidia-smi, go straight to sysfs. + goto try_amd; + } + } +#endif +try_nvidia: // NVIDIA: one query returns used + total for the first GPU. { @@ -4099,12 +4167,18 @@ double SystemInfo::get_global_vram_usage_pct() { } } +try_amd: + #ifdef __linux__ - // AMD (and other DRM GPUs): read used/total from sysfs, taking the busiest card. + // AMD (and other DRM GPUs): read used/total from sysfs. + // On unified-memory APUs (Strix Halo/Point), include GTT in the ratio. try { const std::string drm_path = "/sys/class/drm"; if (fs::exists(drm_path)) { - double highest_ratio = -1.0; + double highest_vram_ratio = -1.0; + double highest_unified_ratio = -1.0; + bool is_unified_gpu = is_strix_apu(); + for (const auto& entry : fs::directory_iterator(drm_path)) { std::string card_name = entry.path().filename().string(); if (card_name.rfind("card", 0) != 0 || card_name.find('-') != std::string::npos) { @@ -4112,8 +4186,7 @@ double SystemInfo::get_global_vram_usage_pct() { } std::string device_path = entry.path().string() + "/device"; - uint64_t vram_used = 0; - uint64_t vram_total = 0; + uint64_t vram_used = 0, vram_total = 0; { std::ifstream f(device_path + "/mem_info_vram_used"); if (f.is_open()) f >> vram_used; @@ -4125,13 +4198,40 @@ double SystemInfo::get_global_vram_usage_pct() { if (vram_total > 0) { double ratio = static_cast(vram_used) / static_cast(vram_total); - if (ratio > highest_ratio) { - highest_ratio = ratio; + if (ratio > highest_vram_ratio) { + highest_vram_ratio = ratio; } } + + // On unified-memory GPUs, also read GTT and compute combined ratio. + if (is_unified_gpu) { + uint64_t gtt_used = 0, gtt_total = 0; + { + std::ifstream f(device_path + "/mem_info_gtt_used"); + if (f.is_open()) f >> gtt_used; + } + { + std::ifstream f(device_path + "/mem_info_gtt_total"); + if (f.is_open()) f >> gtt_total; + } + + if (gtt_total > 0) { + double unified_used = static_cast(vram_used + gtt_used); + double unified_total = static_cast(vram_total + gtt_total); + double ratio = unified_used / unified_total; + if (ratio > highest_unified_ratio) { + highest_unified_ratio = ratio; + } + } + } + } + + // Prefer unified ratio (includes GTT) for Strix APUs, fall back to vram_only. + if (highest_unified_ratio >= 0.0) { + return highest_unified_ratio; } - if (highest_ratio >= 0.0) { - return highest_ratio; + if (highest_vram_ratio >= 0.0) { + return highest_vram_ratio; } } } catch (...) { diff --git a/test/cpp/test_amd_vram.cpp b/test/cpp/test_amd_vram.cpp new file mode 100644 index 0000000000..7971bc3dc0 --- /dev/null +++ b/test/cpp/test_amd_vram.cpp @@ -0,0 +1,106 @@ +// Standalone unit tests for has_unified_memory() — header-only. +// Only requires system_info.h, no linking against system_info.cpp needed. +// +// Compile with: +// g++ -std=c++17 -I src/cpp/include test/cpp/test_amd_vram.cpp -o amd_vram_test + +#include +#include + +#include "lemon/system_info.h" + +static int tests_run = 0; +static int tests_failed = 0; + +#define TEST(name) \ + do { \ + tests_run++; \ + std::cout << " TEST: " << name << " ... "; \ + } while (0) + +#define PASS() \ + do { \ + std::cout << "PASS" << std::endl; \ + } while (0) + +#define FAIL(msg) \ + do { \ + tests_failed++; \ + std::cout << "FAIL (" << msg << ")" << std::endl; \ + } while (0) + +// --- has_unified_memory tests --- + +static void test_has_unified_memory() { + TEST("gfx1105 (Strix Halo) returns true"); + assert(lemon::has_unified_memory(110501) == true); + assert(lemon::has_unified_memory(110500) == true); + PASS(); + + TEST("gfx1102 (Strix Point) returns true"); + assert(lemon::has_unified_memory(110201) == true); + assert(lemon::has_unified_memory(110200) == true); + PASS(); + + TEST("gfx1103 (RDNA3 desktop) returns false"); + assert(lemon::has_unified_memory(110301) == false); + assert(lemon::has_unified_memory(110300) == false); + PASS(); + + TEST("gfx1150 (CDNA) returns false"); + assert(lemon::has_unified_memory(115001) == false); + PASS(); + + TEST("gfx1201 (RDNA4 desktop) returns false"); + assert(lemon::has_unified_memory(120101) == false); + PASS(); + + TEST("zero gfx returns false"); + assert(lemon::has_unified_memory(0) == false); + PASS(); +} + +// --- vram ratio calculation verification --- + +static void test_vram_ratio_calculation() { + // Verify the expected ratio for the values seen on this machine: + // vram: 154951680 / 2147483648 → 7.22% + // gtt: 18677760 / 133143986176 → 0.014% + // unified: (154951680 + 18677760) / (2147483648 + 133143986176) + // = 173629440 / 135291469824 ≈ 0.001283 ≈ 0.13% + double vram_used = 154951680.0; + double vram_total = 2147483648.0; + double gtt_used = 18677760.0; + double gtt_total = 133143986176.0; + + double vram_ratio = vram_used / vram_total; + double unified_ratio = (vram_used + gtt_used) / (vram_total + gtt_total); + + TEST("vram-only ratio ≈ 7.22%"); + std::cout << " vram ratio = " << (vram_ratio * 100.0) << "%"; + assert(vram_ratio > 0.07 && vram_ratio < 0.08); + std::cout << " — PASS" << std::endl; + + TEST("unified ratio ≈ 0.13%"); + std::cout << " unified ratio = " << (unified_ratio * 100.0) << "%"; + assert(unified_ratio > 0.001 && unified_ratio < 0.002); + std::cout << " — PASS" << std::endl; + + TEST("unified ratio << vram ratio (0.13% << 7.22%)"); + assert(unified_ratio < vram_ratio); + std::cout << " " << (vram_ratio / unified_ratio) << "x less pressure with GTT — PASS" << std::endl; +} + +int main() { + std::cout << "=== AMD VRAM Unified Memory Tests ===" << std::endl; + std::cout << std::endl; + + test_has_unified_memory(); + test_vram_ratio_calculation(); + + std::cout << std::endl; + std::cout << "Results: " << (tests_run - tests_failed) << "/" << tests_run + << " passed, " << tests_failed << " failed" << std::endl; + + return tests_failed > 0 ? 1 : 0; +}