-
-
Notifications
You must be signed in to change notification settings - Fork 6.2k
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 35 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,114 @@ | ||
| # SPDX-License-Identifier: AGPL-3.0-only | ||
| # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 | ||
|
|
||
| """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>\\t<total_bytes>`` 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. ``total_bytes`` is the device-local heap size, which the | ||
| reader uses to reserve absolute headroom on a discrete card (parity with the | ||
| CUDA/ROCm fit); it is ignored for an iGPU, whose "VRAM" is shared system RAM. | ||
|
|
||
| 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] | ||
|
|
||
| # Hold add_dll_directory's handle in a frame local for the rest of main() | ||
| # (through the CDLL loads below). CPython's os._AddedDllDirectory has no | ||
| # __del__, so this isn't strictly required, but keeping the reference is the | ||
| # documented idiom and removes any doubt that bindir stays on the search path | ||
| # while the sibling ggml DLLs resolve. | ||
| _dll_dir = None | ||
| if sys.platform == "win32": | ||
| base_name, vk_name = "ggml-base.dll", "ggml-vulkan.dll" | ||
| try: | ||
| _dll_dir = os.add_dll_directory(bindir) | ||
| 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) | ||
| lib.ggml_backend_vk_get_device_memory(i, ctypes.byref(free), ctypes.byref(total)) | ||
| rows.append("%d\t%d\t%d\t%d" % (i, free.value, int(igpu[i]), total.value)) | ||
|
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 hosts where Vulkan enumerates a skipped CPU/llvmpipe device or duplicate ICD before a real GPU, this 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. Confirmed genuine, and it is a real P1: I read ggml-org/llama.cpp ggml-vulkan.cpp and you are right that GGML_VK_VISIBLE_DEVICES is parsed in the raw vkEnumeratePhysicalDevices index space (verbatim, no CPU/llvmpipe filtering, in the env-set branch), while ggml_backend_vk_get_device_count/get_device_memory index the compact post-filter device_indices list. So the probe's compact ordinal is not the mask's raw index, and on a host where a filtered device (lavapipe, a duplicate ICD) sorts before a real GPU, both the mask filter and the pin writeback target the wrong device. The correct fix pins via llama-server --device Vulkan (compact registry space, same as the probe) and lets ggml apply the user mask itself instead of the Python-side filter. That reworks the core device-selection, so I am validating the approach before landing it rather than swapping the pin blind. Not marking this resolved yet.
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 1808b20. Reworked the whole Vulkan device path onto the compact/registry space so probe, mask, and pin no longer conflate it with the raw GGML_VK_VISIBLE_DEVICES index space: the probe stops popping the mask and no longer filters in Python (ggml applies the mask itself, so probe and launch enumerate the same list), and load_model pins via --device Vulkan (new _vulkan_pin_args) instead of writing a compact ordinal into GGML_VK_VISIBLE_DEVICES. On a host where a CPU/llvmpipe device or duplicate ICD sorts before a real GPU, the old code could launch on the wrong device or the software rasterizer; the new code selects the probed device by name. Verified --device and Vulkan naming against the llama-server --help and the ggml-vulkan source, and swept that --device survives the fit-off/flash-attn-off/text-only cmd rebuilds and that no other launch path pinned Vulkan. One thing I could not exercise here (no Vulkan GPU on this box): a live smoke test that llama-server accepts --device Vulkan0 and offloads to the intended physical GPU on a real multi-GPU/Vulkan host. |
||
| sys.stdout.write("\n".join(rows)) | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) | ||
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.