Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
15 changes: 15 additions & 0 deletions src/cpp/include/lemon/system_info.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
122 changes: 111 additions & 11 deletions src/cpp/server/system_info.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint32_t>(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.
{
Expand Down Expand Up @@ -4099,21 +4167,26 @@ 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) {
continue;
}
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;
Expand All @@ -4125,13 +4198,40 @@ double SystemInfo::get_global_vram_usage_pct() {

if (vram_total > 0) {
double ratio = static_cast<double>(vram_used) / static_cast<double>(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<double>(vram_used + gtt_used);
double unified_total = static_cast<double>(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 (...) {
Expand Down
106 changes: 106 additions & 0 deletions test/cpp/test_amd_vram.cpp
Original file line number Diff line number Diff line change
@@ -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 <cassert>
#include <iostream>

#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;
}
Loading