-
-
Notifications
You must be signed in to change notification settings - Fork 6.3k
Studio: add Vulkan llama.cpp support #5819
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
9e9729b
c401f10
84d98a7
11acf22
7dd21f3
2401515
e50b0af
4fefeeb
ea7cd94
10faad1
dafeb79
1980e59
31f4a36
c3482d4
cca5ca5
7563d91
32b8333
2f32763
f372858
6fb4a3a
ad5b678
e8becbf
47d468a
15ed8ed
845d061
0ba53cb
57936d5
dc71fd3
c4b4984
1808b20
737a0fa
8512ccb
5432a0c
f455558
4e968d4
40c4686
f66490b
b0f4997
08e96f4
d1b37e8
576d81c
bd50e5c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| """Standalone free-VRAM probe for the bundled ggml Vulkan backend. | ||
|
|
||
| Run in a short-lived subprocess (``python _vulkan_probe.py <bindir>``) so the | ||
| Vulkan instance never lives in the long-running backend process. Loads the | ||
| bundled ggml Vulkan backend from ``<bindir>`` and prints one | ||
| ``<idx>\\t<free_bytes>\\t<is_igpu>`` line per device to stdout. The indices | ||
| are ggml's own Vulkan device ordinals (the space GGML_VK_VISIBLE_DEVICES | ||
| expects), which need not match nvidia-smi order. ``is_igpu`` is ``1`` for an | ||
| integrated GPU (shared system RAM) and ``0`` otherwise, taken from ggml's own | ||
| device type so the reader needn't guess from VRAM-vs-RAM ratios. | ||
|
|
||
| Uses only the standard library so it stays runnable as a bare script without | ||
| importing the backend package. | ||
| """ | ||
|
|
||
| import ctypes | ||
| import os | ||
| import sys | ||
|
|
||
| # ggml_backend_dev_type enum (ggml-backend.h): CPU=0, GPU=1, IGPU=2, ... | ||
| _GGML_BACKEND_DEVICE_TYPE_IGPU = 2 | ||
|
|
||
|
|
||
| def _igpu_flags(base, lib, count: int) -> list[bool]: | ||
| """Per-device integrated-GPU flags via ggml's backend registry. | ||
|
|
||
| The Vulkan reg enumerates devices in the same order as | ||
| ``ggml_backend_vk_get_device_memory`` (ggml-vulkan builds each device | ||
| context with ``ctx->device = i``), so reg index == device ordinal. | ||
| Returns all-False on any failure so the reader never over-caps a | ||
| discrete card just because the type couldn't be read. | ||
| """ | ||
| flags = [False] * count | ||
| try: | ||
| lib.ggml_backend_vk_reg.restype = ctypes.c_void_p | ||
| lib.ggml_backend_vk_reg.argtypes = [] | ||
| base.ggml_backend_reg_dev_count.restype = ctypes.c_size_t | ||
| base.ggml_backend_reg_dev_count.argtypes = [ctypes.c_void_p] | ||
| base.ggml_backend_reg_dev_get.restype = ctypes.c_void_p | ||
| base.ggml_backend_reg_dev_get.argtypes = [ctypes.c_void_p, ctypes.c_size_t] | ||
| base.ggml_backend_dev_type.restype = ctypes.c_int | ||
| base.ggml_backend_dev_type.argtypes = [ctypes.c_void_p] | ||
|
|
||
| reg = lib.ggml_backend_vk_reg() | ||
| if not reg: | ||
| return flags | ||
| dev_count = base.ggml_backend_reg_dev_count(reg) | ||
| for i in range(min(count, dev_count)): | ||
| dev = base.ggml_backend_reg_dev_get(reg, i) | ||
| if dev: | ||
| flags[i] = ( | ||
| base.ggml_backend_dev_type(dev) == _GGML_BACKEND_DEVICE_TYPE_IGPU | ||
| ) | ||
| except Exception: | ||
| # iGPU detection is best-effort: any failure (missing symbol, | ||
| # registry call error) degrades to "discrete" so the memory | ||
| # readings still get through instead of crashing the probe. | ||
| pass | ||
| return flags | ||
|
|
||
|
|
||
| def main() -> int: | ||
| if len(sys.argv) < 2: | ||
| return 0 | ||
| bindir = sys.argv[1] | ||
|
|
||
| if sys.platform == "win32": | ||
| base_name, vk_name = "ggml-base.dll", "ggml-vulkan.dll" | ||
| try: | ||
| os.add_dll_directory(bindir) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On Windows Vulkan installs, the object returned by Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a bug. os._AddedDllDirectory (Lib/os.py, Windows-only) defines only close/enter/exit, no del, so garbage-collecting the discarded handle does not remove the directory; it stays on the DLL search path until an explicit close (which never happens) or process exit. The two CDLL loads run on the next lines while bindir is still present. Verified against CPython 3.12 os.py. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On Windows, Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as the earlier round: not a bug. I read the installed CPython os.py (3.12) directly -- os._AddedDllDirectory (the Windows-only class add_dll_directory returns) defines only init/close/enter/exit/repr, with no del and no weakref finalizer, so garbage-collecting the discarded handle never removes the directory. It stays on the DLL search path until an explicit close (which never happens here) or process exit, and the two CDLL loads run on the next lines while it is still present. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On Windows Vulkan installs where Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Third time on this one and still not a bug: I read the installed CPython 3.12 os.py directly. os._AddedDllDirectory (Windows-only) defines init/close/enter/exit/repr only, with no del and no weakref finalizer, so garbage-collecting the discarded handle does not remove the directory. It stays on the DLL search path until an explicit close (never called here) or process exit, and the two CDLL loads run on the very next lines. Happy to revisit with a concrete repro on a real Windows Vulkan host, but the GC-drop premise does not hold in CPython. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On Windows, Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same finding a fourth time, same verdict: verified false positive. CPython's os._AddedDllDirectory (Windows-only) has no del and no weakref finalizer (I read the installed 3.12 Lib/os.py), so garbage-collecting the discarded handle never removes the directory; it persists until an explicit close (never called) or process exit, and the CDLL loads run on the next lines. This exact code path was in the commit Codex approved (737a0fa). If there is a concrete repro on a real Windows Vulkan host where the sibling DLLs fail to resolve, I will act on it, but the GC-drop premise does not hold in CPython. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On Windows, Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fifth time, unchanged verdict: verified false positive. os._AddedDllDirectory has no del/finalizer in CPython, so the discarded handle is never GC-closed and bindir stays on the DLL search path until process exit; the CDLL loads run immediately after. This same code was in the commit you approved (737a0fa). Not changing it without a concrete Windows-Vulkan repro.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in 5432a0c. To be clear on the mechanism: os._AddedDllDirectory has no del/finalizer in CPython, so the discarded handle was never GC-closed and bindir already stayed on the search path through the loads. But keeping the handle is the documented idiom and costs nothing, so I now hold it in a frame local for the rest of the probe. This removes any ambiguity without relying on that GC reasoning. |
||
| except Exception: | ||
| pass | ||
| else: | ||
| base_name, vk_name = "libggml-base.so", "libggml-vulkan.so" | ||
|
|
||
| try: | ||
| base = ctypes.CDLL(os.path.join(bindir, base_name), mode = ctypes.RTLD_GLOBAL) | ||
| lib = ctypes.CDLL(os.path.join(bindir, vk_name), mode = ctypes.RTLD_GLOBAL) | ||
|
oobabooga marked this conversation as resolved.
Outdated
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On Windows Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed in 40c4686 with getattr(ctypes, 'RTLD_GLOBAL', 0). To be accurate on the mechanism: ctypes.RTLD_GLOBAL is defined on every platform, including Windows, where CPython's _ctypes falls back to 0 (#ifdef RTLD_GLOBAL #else 0), and CDLL ignores mode on Windows and uses LoadLibraryEx, so there was no AttributeError and the probe already loaded. The getattr form is behaviorally identical and makes the cross-platform default explicit, so I've applied it to close this out. |
||
| except OSError as e: | ||
| print(f"ggml-vulkan load failed: {e}", file = sys.stderr) | ||
| return 1 | ||
|
|
||
| lib.ggml_backend_vk_get_device_count.restype = ctypes.c_int | ||
| lib.ggml_backend_vk_get_device_count.argtypes = [] | ||
| lib.ggml_backend_vk_get_device_memory.restype = None | ||
| lib.ggml_backend_vk_get_device_memory.argtypes = [ | ||
| ctypes.c_int, | ||
| ctypes.POINTER(ctypes.c_size_t), | ||
| ctypes.POINTER(ctypes.c_size_t), | ||
| ] | ||
|
|
||
| count = lib.ggml_backend_vk_get_device_count() | ||
| igpu = _igpu_flags(base, lib, count) | ||
| rows = [] | ||
| for i in range(count): | ||
| free, total = ctypes.c_size_t(0), ctypes.c_size_t(0) | ||
| # total is a required out-param of the C call but unused: the reader | ||
| # leaves a flat per-device margin, not a fraction of total. | ||
| lib.ggml_backend_vk_get_device_memory( | ||
| i, ctypes.byref(free), ctypes.byref(total) | ||
| ) | ||
| rows.append("%d\t%d\t%d" % (i, free.value, int(igpu[i]))) | ||
| sys.stdout.write("\n".join(rows)) | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -607,6 +607,42 @@ def _backfill_usage_from_timings(usage, timings): | |
| return out | ||
|
|
||
|
|
||
| def _vulkan_lib_filename() -> str: | ||
| return "ggml-vulkan.dll" if sys.platform == "win32" else "libggml-vulkan.so" | ||
|
|
||
|
|
||
| # Free system RAM to leave on an integrated GPU, mirroring llama.cpp's own | ||
| # auto-fit margin (llama-server --fit-target, default 1024 MiB per device). | ||
| # ggml reports an iGPU's "VRAM" as shared system RAM, so we hold back the same | ||
| # per-device margin --fit would rather than inventing a larger reserve. | ||
| _IGPU_HOST_RESERVE_MIB = 1024 | ||
|
|
||
|
|
||
| def _apply_igpu_host_reserve_mib(free_mib: int, is_igpu: bool) -> int: | ||
| """Reserve host headroom on an integrated (shared-memory) Vulkan GPU. | ||
|
|
||
| ggml sums every memory heap for an integrated GPU (ggml-vulkan's | ||
| ggml_backend_vk_get_device_memory), so its reported free "VRAM" is really | ||
| free system RAM. Sizing context/offload against all of it would crowd out | ||
| the host and push it into swap or the OOM killer. We leave the same | ||
| per-device margin llama.cpp's --fit uses (``_IGPU_HOST_RESERVE_MIB``). | ||
| ``is_igpu`` comes straight from ggml's device type, so a discrete card is | ||
| never touched. Only ever reduces the budget. | ||
| """ | ||
| if not is_igpu: | ||
| return free_mib | ||
| return max(0, free_mib - _IGPU_HOST_RESERVE_MIB) | ||
|
|
||
|
|
||
| def _llama_lib_dir(binary: str) -> Path: | ||
| # The installer exposes llama-server as a top-level symlink | ||
| # (~/.unsloth/llama.cpp/llama-server) into build/bin/, where the ggml | ||
| # backend libs actually live. Resolve it so callers looking for sibling | ||
| # libs (Vulkan detection, LD_LIBRARY_PATH, the probe's bindir) hit the real | ||
| # directory instead of the symlink's parent. | ||
| return Path(binary).resolve().parent | ||
|
|
||
|
|
||
| class LlamaCppBackend: | ||
| """ | ||
| Manages a llama-server subprocess for GGUF model inference. | ||
|
|
@@ -1236,6 +1272,20 @@ def _get_gguf_size_bytes(model_path: str) -> int: | |
|
|
||
| return total | ||
|
|
||
| @staticmethod | ||
| def _is_vulkan_backend(binary: Optional[str] = None) -> bool: | ||
| """True if the installed llama.cpp build is the Vulkan one. | ||
|
|
||
| Builds are single-backend, so the presence of the Vulkan ggml | ||
| backend library next to llama-server is sufficient. Used to keep | ||
| the free-memory probe and the GPU pin in the same device-index | ||
| space (ggml's Vulkan ordinals, not nvidia-smi order). | ||
| """ | ||
| binary = binary or LlamaCppBackend._find_llama_server_binary() | ||
| if not binary: | ||
| return False | ||
| return (_llama_lib_dir(binary) / _vulkan_lib_filename()).is_file() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Studio is pointed at a local/custom llama.cpp build that includes multiple ggml backends, the presence of Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in f455558. _is_vulkan_backend now returns True only when Vulkan is the sole GPU backend: if a libggml-cuda or libggml-hip sibling sits next to libggml-vulkan (a custom multi-backend build), it defers to the CUDA/HIP path (nvidia-smi/torch probes and the CUDA/HIP pin). Official prebuilts are single-backend so they're unaffected. Tests: test_multi_backend_build_is_not_vulkan_only and test_vulkan_only_build_is_detected. |
||
|
|
||
| @staticmethod | ||
| def _amd_apu_wants_unified_memory() -> bool: | ||
| """True only for AMD unified-memory APUs (gfx1150/gfx1151), where | ||
|
|
@@ -1264,7 +1314,24 @@ def _amd_apu_wants_unified_memory() -> bool: | |
| return False | ||
|
|
||
| @staticmethod | ||
| def _get_gpu_free_memory() -> list[tuple[int, int]]: | ||
| def _get_gpu_free_memory(binary: Optional[str] = None) -> list[tuple[int, int]]: | ||
| """Query free memory per GPU across all supported backends. | ||
|
|
||
| On a Vulkan build, the ggml Vulkan probe is authoritative so the | ||
| returned indices are Vulkan ordinals (the space the GPU pin writes | ||
| to ``GGML_VK_VISIBLE_DEVICES``). Otherwise ``nvidia-smi`` / torch | ||
| cover NVIDIA + AMD ROCm. | ||
|
|
||
| Returns list of (gpu_index, free_mib) sorted by index. Empty | ||
| list if no supported GPU is reachable. | ||
| """ | ||
| binary = binary or LlamaCppBackend._find_llama_server_binary() | ||
| if LlamaCppBackend._is_vulkan_backend(binary): | ||
| return LlamaCppBackend._get_gpu_free_memory_vulkan(binary) | ||
| return LlamaCppBackend._get_gpu_free_memory_nvidia_torch() | ||
|
|
||
| @staticmethod | ||
| def _get_gpu_free_memory_nvidia_torch() -> list[tuple[int, int]]: | ||
| """Query free memory per GPU. | ||
|
|
||
| Order: | ||
|
|
@@ -1385,6 +1452,87 @@ def _get_gpu_free_memory() -> list[tuple[int, int]]: | |
| logger.debug(f"torch GPU probe failed: {e}") | ||
| return [] | ||
|
|
||
| @staticmethod | ||
| def _get_gpu_free_memory_vulkan( | ||
| binary: Optional[str] = None, | ||
| ) -> list[tuple[int, int]]: | ||
| """Query free VRAM per device via the bundled ggml Vulkan backend. | ||
|
|
||
| Loads ``libggml-vulkan`` in a short-lived subprocess and calls | ||
| ``ggml_backend_vk_get_device_memory`` for each device, so no Vulkan | ||
| instance is created in this process. Returns list of | ||
| (device_index, free_mib) sorted by index, where the index is ggml's | ||
| own Vulkan device ordinal (the space ``GGML_VK_VISIBLE_DEVICES`` | ||
| expects). Integrated GPUs leave a per-device host-RAM margin (see | ||
| ``_apply_igpu_host_reserve_mib``). Returns [] when no Vulkan build is | ||
| installed or no device is reachable. | ||
| """ | ||
| binary = binary or LlamaCppBackend._find_llama_server_binary() | ||
| if not binary: | ||
| return [] | ||
| binary_dir = _llama_lib_dir(binary) | ||
| if not (binary_dir / _vulkan_lib_filename()).is_file(): | ||
| return [] | ||
|
|
||
| env = child_env_without_native_path_secret() | ||
| # Enumerate ggml's canonical, full device list. An inherited | ||
| # GGML_VK_VISIBLE_DEVICES would renumber/restrict the ordinals, but | ||
| # load_model writes its own pin in that same full space, so letting | ||
| # the probe see a pre-existing mask would make the pin double-apply | ||
| # and target the wrong device. | ||
| env.pop("GGML_VK_VISIBLE_DEVICES", None) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On a multi-GPU Vulkan host where the user sets Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in e8becbf. The Vulkan reader now filters the enumerated devices by an inherited GGML_VK_VISIBLE_DEVICES the same way the nvidia-smi path filters CUDA_VISIBLE_DEVICES, so a device the user hid can no longer be ranked by the fit or written back by the pin. The probe still strips the mask in its own subprocess to keep ggml's full-space ordinals; the parent applies the mask against that same space. Regression test: test_inherited_visible_devices_mask_filters_hidden_device. |
||
| if sys.platform != "win32": | ||
| # Let the loader resolve sibling ggml libs next to the binary. | ||
| existing_ld = env.get("LD_LIBRARY_PATH", "") | ||
| env["LD_LIBRARY_PATH"] = ( | ||
| f"{binary_dir}:{existing_ld}" if existing_ld else str(binary_dir) | ||
| ) | ||
| probe_script = Path(__file__).with_name("_vulkan_probe.py") | ||
| try: | ||
| result = subprocess.run( | ||
| [sys.executable, str(probe_script), str(binary_dir)], | ||
| capture_output = True, | ||
| text = True, | ||
| timeout = 15, | ||
| env = env, | ||
| **_windows_hidden_subprocess_kwargs(), | ||
| ) | ||
| if result.returncode != 0: | ||
| logger.debug( | ||
| f"vulkan GPU probe exited {result.returncode}: {result.stderr.strip()}" | ||
| ) | ||
| return [] | ||
| except Exception as e: | ||
|
oobabooga marked this conversation as resolved.
|
||
| logger.debug(f"vulkan GPU probe failed: {e}") | ||
| return [] | ||
|
|
||
| gpus: list[tuple[int, int]] = [] | ||
| for line in result.stdout.strip().splitlines(): | ||
| parts = line.split("\t") | ||
| if len(parts) != 3: | ||
| continue | ||
| try: | ||
| idx = int(parts[0]) | ||
| free_mib = int(parts[1]) // (1024 * 1024) | ||
| is_igpu = parts[2] == "1" | ||
| except ValueError: | ||
| continue | ||
| capped = _apply_igpu_host_reserve_mib(free_mib, is_igpu) | ||
| if capped < free_mib: | ||
| logger.info( | ||
| f"Vulkan device VK{idx} is an integrated GPU sharing system " | ||
| f"RAM; reserving {free_mib - capped}MiB host headroom " | ||
| f"({free_mib}->{capped}MiB usable)" | ||
| ) | ||
| gpus.append((idx, capped)) | ||
| gpus.sort(key = lambda g: g[0]) | ||
| if gpus: | ||
| logger.info( | ||
| "Vulkan GPU memory detected: " | ||
| + ", ".join(f"VK{idx}={free}MiB" for idx, free in gpus) | ||
| ) | ||
| return gpus | ||
|
|
||
| # Skip the wait when the last kill is older than this; the GPU | ||
| # driver has already reclaimed the prior process's allocations. | ||
| _VRAM_SETTLE_WINDOW_S: float = 15.0 | ||
|
|
@@ -2798,6 +2946,7 @@ def load_model( | |
| "Run setup.sh to build it, install llama.cpp, " | ||
| "or set LLAMA_SERVER_PATH environment variable." | ||
| ) | ||
| is_vulkan_backend = self._is_vulkan_backend(binary) | ||
|
|
||
| # ── Phase 2: download (NO lock held, so cancel can proceed) ── | ||
| # Scope HF_HUB_OFFLINE to the download block only when DNS is | ||
|
|
@@ -2873,7 +3022,7 @@ def load_model( | |
| gpus: list[tuple[int, int]] = [] | ||
| try: | ||
| model_size = self._get_gguf_size_bytes(model_path) | ||
| gpus = self._get_gpu_free_memory() | ||
| gpus = self._get_gpu_free_memory(binary) | ||
|
|
||
| # Resolve effective context: 0 means let llama-server use the | ||
| # model's native length. Only expand to a known native length | ||
|
|
@@ -3278,7 +3427,7 @@ def load_model( | |
| import sys | ||
|
|
||
| env = child_env_without_native_path_secret() | ||
| binary_dir = str(Path(binary).parent) | ||
| binary_dir = str(_llama_lib_dir(binary)) | ||
|
|
||
| # AMD unified-memory APUs (gfx1150/gfx1151): let llama.cpp use | ||
| # shared system RAM. setdefault so a user value wins. | ||
|
|
@@ -3387,17 +3536,24 @@ def load_model( | |
| # the full HIP/ROCR set the parent inherited. | ||
| if gpu_indices is not None: | ||
| pinned = ",".join(str(i) for i in gpu_indices) | ||
| env["CUDA_VISIBLE_DEVICES"] = pinned | ||
| try: | ||
| import torch as _torch | ||
|
|
||
| if getattr(_torch.version, "hip", None) is not None: | ||
| env["HIP_VISIBLE_DEVICES"] = pinned | ||
| env["ROCR_VISIBLE_DEVICES"] = pinned | ||
| except Exception as e: | ||
| logger.debug( | ||
| "Failed to set ROCm visibility env vars for child: %s", e | ||
| ) | ||
| if is_vulkan_backend: | ||
| # gpu_indices are ggml Vulkan ordinals (see | ||
| # _get_gpu_free_memory); the Vulkan backend ignores | ||
| # CUDA_VISIBLE_DEVICES, so pin via its own mask. | ||
| env["GGML_VK_VISIBLE_DEVICES"] = pinned | ||
| else: | ||
| env["CUDA_VISIBLE_DEVICES"] = pinned | ||
| try: | ||
| import torch as _torch | ||
|
|
||
| if getattr(_torch.version, "hip", None) is not None: | ||
| env["HIP_VISIBLE_DEVICES"] = pinned | ||
| env["ROCR_VISIBLE_DEVICES"] = pinned | ||
| except Exception as e: | ||
| logger.debug( | ||
| "Failed to set ROCm visibility env vars for child: %s", | ||
| e, | ||
| ) | ||
|
|
||
| # Defensive kill: if a concurrent load slipped past Phase 1 | ||
| # (because its `self._process` was None at the time) and | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the upstream shared-library layout, the registry APIs (
ggml_backend_reg_dev_count/ggml_backend_reg_dev_get) live inlibggml.so, notlibggml-base.so; looking them up on thebasehandle here makes_igpu_flags()catch anAttributeErrorand return all-False. On Vulkan integrated-GPU installs this means the parent never applies the iGPU host-memory reserve and passes the shared-memorytotalthrough as if it were discrete VRAM, so context fitting can over-budget host RAM. Load/use the ggml registry library for these calls instead oflibggml-base.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a bug. nm -D on the actual upstream Vulkan prebuilt (b9935) shows ggml_backend_reg_dev_count, ggml_backend_reg_dev_get and ggml_backend_dev_type are all defined and exported (T) in libggml-base.so; in libggml.so they are only imported (undefined U), and libggml-vulkan.so lists libggml-base.so.0 in DT_NEEDED. So loading libggml-base and looking these up on that handle resolves cleanly (verified via ctypes), _igpu_flags reads the device type, and the iGPU host-reserve / total=0 path works. Left as is.