Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
9e9729b
Studio: add Vulkan llama.cpp support
oobabooga May 27, 2026
c401f10
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 27, 2026
84d98a7
Address gemini's feedback
oobabooga May 27, 2026
11acf22
Studio: move the Vulkan VRAM probe into a standalone script
oobabooga May 27, 2026
7dd21f3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 27, 2026
2401515
Merge branch 'main' into vulkan-support
Imagineer99 May 27, 2026
e50b0af
Improve Vulkan probe error reporting
oobabooga May 28, 2026
4fefeeb
Resolve llama-server symlink so Vulkan build is detected
oobabooga May 29, 2026
ea7cd94
Merge branch 'main' into vulkan-support
oobabooga May 31, 2026
10faad1
Drop unreachable Vulkan fallback in GPU free-memory dispatcher
oobabooga May 31, 2026
dafeb79
Skip the Intel GPU probe when NVIDIA or ROCm is present
oobabooga May 31, 2026
1980e59
Reserve host RAM headroom for Vulkan integrated GPUs
oobabooga May 31, 2026
31f4a36
Add a `UNSLOTH_FORCE_VULKAN` environment variable
oobabooga May 31, 2026
c3482d4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 31, 2026
cca5ca5
Merge branch 'main' into vulkan-support
oobabooga Jun 9, 2026
7563d91
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 9, 2026
32b8333
Merge branch 'main' into vulkan-support
oobabooga Jun 12, 2026
2f32763
Merge branch 'main' into vulkan-support
oobabooga Jun 20, 2026
f372858
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 20, 2026
6fb4a3a
Merge branch 'main' into vulkan-support
oobabooga Jun 22, 2026
ad5b678
Merge branch 'main' into vulkan-support
oobabooga Jul 3, 2026
e8becbf
Honor GGML_VK_VISIBLE_DEVICES, reserve discrete Vulkan VRAM headroom,…
oobabooga Jul 8, 2026
47d468a
Merge remote-tracking branch 'origin/main' into r5819
oobabooga Jul 8, 2026
15ed8ed
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 8, 2026
845d061
Route Intel and forced-Vulkan hosts to the upstream Vulkan prebuilt, …
oobabooga Jul 8, 2026
0ba53cb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 8, 2026
57936d5
Clear the fork release pin when routing a Vulkan host to the upstream…
oobabooga Jul 8, 2026
dc71fd3
Gate auto-Vulkan routing on no physical NVIDIA so hidden CUDA devices…
oobabooga Jul 8, 2026
c4b4984
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 8, 2026
1808b20
Pin Vulkan launches with --device Vulkan<i> instead of the raw GGML_V…
oobabooga Jul 8, 2026
737a0fa
Let user --device override the Vulkan pin, and gate direct Vulkan ass…
oobabooga Jul 8, 2026
8512ccb
Update RAG auto-backend test mocks for the _resolve_auto binary and V…
oobabooga Jul 8, 2026
5432a0c
Keep the add_dll_directory handle alive through the Vulkan probe DLL …
oobabooga Jul 8, 2026
f455558
Revert RAG auto Vulkan guard, guard multi-backend Vulkan detection, a…
oobabooga Jul 8, 2026
4e968d4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 8, 2026
40c4686
Use getattr for RTLD_GLOBAL in the Vulkan probe CDLL mode
oobabooga Jul 8, 2026
f66490b
Skip CUDA/ROCm APU and datacenter GPU tuning on Vulkan builds
danielhanchen Jul 9, 2026
b0f4997
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 9, 2026
08e96f4
Tighten Vulkan-guard comment in load_model
danielhanchen Jul 9, 2026
d1b37e8
Reduce comments in Vulkan support to be more succinct
danielhanchen Jul 9, 2026
576d81c
Resolve shell-wrapper llama-server entrypoint to the real lib dir
danielhanchen Jul 9, 2026
bd50e5c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 9, 2026
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
114 changes: 114 additions & 0 deletions studio/backend/core/inference/_vulkan_probe.py
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]
Comment on lines +39 to +44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Load ggml registry symbols from libggml

In the upstream shared-library layout, the registry APIs (ggml_backend_reg_dev_count / ggml_backend_reg_dev_get) live in libggml.so, not libggml-base.so; looking them up on the base handle here makes _igpu_flags() catch an AttributeError and return all-False. On Vulkan integrated-GPU installs this means the parent never applies the iGPU host-memory reserve and passes the shared-memory total through as if it were discrete VRAM, so context fitting can over-budget host RAM. Load/use the ggml registry library for these calls instead of libggml-base.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

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.


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)
Comment thread
oobabooga marked this conversation as resolved.
Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a Windows-safe CDLL mode

On Windows ctypes does not define RTLD_GLOBAL, so the attribute lookup here raises AttributeError before ctypes.CDLL runs, and the except OSError block does not catch it. For every Windows Vulkan install the probe subprocess exits non-zero and _get_gpu_free_memory_vulkan() returns [], which removes Vulkan VRAM sizing and device pinning even though the bundle is installed; use getattr(ctypes, "RTLD_GLOBAL", 0) or omit mode on Windows.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use raw Vulkan device IDs for the visible-device mask

On hosts where Vulkan enumerates a skipped CPU/llvmpipe device or duplicate ICD before a real GPU, this i is only the compact ggml device ordinal accepted by ggml_backend_vk_get_device_memory, but the caller later compares it with and writes it back to GGML_VK_VISIBLE_DEVICES. ggml parses that environment variable in the raw Vulkan physical-device index space before building the filtered/deduplicated device list, so a user mask like GGML_VK_VISIBLE_DEVICES=2 can be filtered out here, or Studio can pin 1 and launch on a different GPU than the one whose memory was probed. Please emit/map the raw Vulkan device index, or avoid rewriting the mask with compact ordinals.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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())
Loading
Loading