Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
108 changes: 108 additions & 0 deletions studio/backend/core/inference/_vulkan_probe.py
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]
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]

if sys.platform == "win32":
base_name, vk_name = "ggml-base.dll", "ggml-vulkan.dll"
try:
os.add_dll_directory(bindir)

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 Retain the DLL directory handle for the Windows probe

On Windows Vulkan installs, the object returned by os.add_dll_directory is discarded immediately. Elsewhere in the backend these handles are kept alive because the DLL search entry is removed when the handle is garbage-collected, so this can drop bindir before ctypes loads ggml-vulkan.dll and its sibling dependencies. In that case _get_gpu_free_memory_vulkan falls back to an empty probe result and Studio launches without Vulkan memory sizing or pinning; keep the handle alive until the CDLL loads complete or until process exit.

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.

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.

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 Keep the DLL directory handle alive

On Windows, os.add_dll_directory() returns the handle that keeps the directory in the DLL search path; discarding it allows the directory to be removed before the ctypes.CDLL calls below resolve the Vulkan backend's sibling DLL dependencies. On Windows Vulkan installs that rely on bundled ggml DLLs in bindir, the probe can fail to load and Studio will miss Vulkan VRAM, so keep the returned handle alive through the DLL loads or use a with block.

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.

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.

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 Keep the Windows DLL directory handle alive

On Windows Vulkan installs where ggml-vulkan.dll has sibling DLL dependencies, the object returned by os.add_dll_directory() is discarded immediately, so CPython can close it before the following ctypes.CDLL calls resolve those dependencies. In that case the probe reports no Vulkan GPUs even though the bundle is valid; keep the returned handle referenced until after both DLLs are loaded.

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.

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.

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 Keep the DLL directory handle alive on Windows

On Windows, os.add_dll_directory() only keeps bindir in the DLL search path while the returned handle remains alive; because the handle is discarded here, the directory can be removed before ctypes.CDLL loads ggml-vulkan.dll and its sibling dependencies. On a Windows Vulkan install where the ggml DLLs depend on other DLLs in build/bin, the probe exits with a load failure and Studio loses Vulkan memory detection. Store the returned handle until after the DLLs are loaded.

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.

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.

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 Keep the DLL directory handle alive on Windows

On Windows, os.add_dll_directory() returns the handle that keeps bindir in the ctypes DLL search path; discarding it lets CPython close/remove the directory before the following ctypes.CDLL loads resolve sibling dependencies. For Windows Vulkan prebuilts whose ggml DLLs depend on other DLLs in the same extracted directory, the probe can fail to load and report no Vulkan GPUs, so keep the returned cookie alive until after the DLLs are loaded (or use a with).

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.

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.

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 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)
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)
# 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())
184 changes: 170 additions & 14 deletions studio/backend/core/inference/llama_cpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()

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 Prefer CUDA/HIP when a build also ships Vulkan

When Studio is pointed at a local/custom llama.cpp build that includes multiple ggml backends, the presence of libggml-vulkan now makes the whole runtime behave as Vulkan-only. That bypasses the existing NVIDIA/ROCm memory probes and later emits Vulkan device names, so a build that also has CUDA or HIP can be sized and launched against the wrong backend (or report no GPU if the Vulkan probe cannot enumerate devices). Please detect the selected install kind or prefer CUDA/HIP backend libraries before treating this sibling file as exclusive.

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.

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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

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 Respect inherited Vulkan visibility masks

On a multi-GPU Vulkan host where the user sets GGML_VK_VISIBLE_DEVICES to reserve or select specific devices, this probe deletes the mask, ranks the full device list, and the launch path later overwrites the environment with the selected full-space index. That can make Studio use a GPU the user explicitly hid, unlike the CUDA/ROCm paths that honor their visibility masks during probing. Please preserve the inherited Vulkan mask semantics, e.g. by intersecting the full-space probe results with the requested visible set before selection.

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.

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:
Comment thread
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading