# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 """GGML_CUDA_ENABLE_UNIFIED_MEMORY must be set only for AMD unified-memory APUs (gfx1150/gfx1151), never for discrete AMD, NVIDIA, CPU or macOS.""" from __future__ import annotations import sys import types import pytest from core.inference.llama_cpp import LlamaCppBackend def _fake_torch( hip, archs, *, cuda_ok = True, ): t = types.ModuleType("torch") t.version = types.SimpleNamespace(hip = hip) t.cuda = types.SimpleNamespace( is_available = lambda: cuda_ok, device_count = lambda: len(archs), get_device_properties = lambda i: types.SimpleNamespace(gcnArchName = archs[i]), ) return t @pytest.mark.parametrize( "hip,archs,expected", [ ("6.2.0", ["gfx1151:xnack-"], True), # Strix Halo APU (suffix stripped) ("6.2.0", ["gfx1150"], True), # Strix Point APU ("6.2.0", ["gfx1100"], False), # discrete RDNA3 ("6.2.0", ["gfx1201"], False), # discrete RDNA4 ("6.2.0", ["gfx942"], False), # MI300X (data center) (None, ["sm_90"], False), # NVIDIA (no torch.version.hip) ("6.2.0", ["gfx1100", "gfx1151"], True), # mixed dGPU + APU ], ) def test_apu_unified_memory_gating(monkeypatch, hip, archs, expected): monkeypatch.setitem(sys.modules, "torch", _fake_torch(hip, archs)) assert LlamaCppBackend._amd_apu_wants_unified_memory() is expected def test_apu_guard_scopes_to_selected_gpu(monkeypatch): # Mixed host: physical id 0 = discrete gfx1100, 1 = gfx1151 APU. for _m in ("HIP_VISIBLE_DEVICES", "ROCR_VISIBLE_DEVICES", "CUDA_VISIBLE_DEVICES"): monkeypatch.delenv(_m, raising = False) monkeypatch.setitem(sys.modules, "torch", _fake_torch("6.2.0", ["gfx1100", "gfx1151"])) # Selecting only the dGPU, or an empty selection, must not be unified-memory. assert LlamaCppBackend._amd_apu_wants_unified_memory([0]) is False assert LlamaCppBackend._amd_apu_wants_unified_memory([]) is False # Selecting the APU, or no selection, does. assert LlamaCppBackend._amd_apu_wants_unified_memory([1]) is True assert LlamaCppBackend._amd_apu_wants_unified_memory() is True def test_apu_guard_honors_hip_visible_devices_mask(monkeypatch): # ROCm resolves ids via HIP first: the mask exposes only the APU as ordinal 0 # but physical id 1, so the selection [1] must still match. monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising = False) monkeypatch.delenv("ROCR_VISIBLE_DEVICES", raising = False) monkeypatch.setenv("HIP_VISIBLE_DEVICES", "1") monkeypatch.setitem(sys.modules, "torch", _fake_torch("6.2.0", ["gfx1151"])) assert LlamaCppBackend._amd_apu_wants_unified_memory([1]) is True assert LlamaCppBackend._amd_apu_wants_unified_memory([0]) is False def test_cpu_no_cuda_returns_false(monkeypatch): monkeypatch.setitem(sys.modules, "torch", _fake_torch("6.2.0", [], cuda_ok = False)) assert LlamaCppBackend._amd_apu_wants_unified_memory() is False def test_missing_torch_returns_false(monkeypatch): monkeypatch.setitem(sys.modules, "torch", None) assert LlamaCppBackend._amd_apu_wants_unified_memory() is False _GB = 1024**3 _MIB_PER_GB = 1024 # Module-level (not a class attr) so it stays a plain function, not a bound method. _shortfall = LlamaCppBackend._apu_ram_shortfall_message class TestApuRamShortfall: """On a unified-memory APU the weights load into system RAM, so a model larger than available RAM (the field case: a 64.6 GB GGUF on a WSL VM capped well below the ROCm-reported APU budget) must be refused before spawning, not left to OOM-kill the Studio process.""" def test_field_case_wsl_cap_refuses(self): # 64.6 GB weights, ~46 GB available (WSL VM): refuse with guidance. msg = _shortfall(int(64.6 * _GB), 46 * _MIB_PER_GB) assert msg is not None assert "65 GB" in msg and "46 GB" in msg assert ".wslconfig" in msg def test_bare_metal_fits_allows(self): # Same model, ~92 GB available (no WSL cap): allow. assert _shortfall(int(64.6 * _GB), 92 * _MIB_PER_GB) is None def test_unknown_available_never_refuses(self): assert _shortfall(int(64.6 * _GB), None) is None def test_boundary_at_headroom(self): # 20 GB weights, headroom 2 GB. avail 23 GB -> fits; 21 GB -> refuse. assert _shortfall(20 * _GB, 23 * _MIB_PER_GB) is None assert _shortfall(20 * _GB, 21 * _MIB_PER_GB) is not None def test_available_system_memory_is_int_or_none(self): v = LlamaCppBackend._available_system_memory_mib() assert v is None or (isinstance(v, int) and v > 0)