# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 """Windows GPU-detection regression test on a synthetic layout. Bug (#5106): on Windows without a system CUDA toolkit, the prebuilt llama-server.exe couldn't LoadLibrary cudart64_X / cublas64_X / cublasLt64_X, so ggml-cuda.dll's static import on cublas64_X.dll failed and the model fell back to CPU even when nvidia-smi reported the GPU. Fix: * #5322 overlays upstream's paired cudart bundle into install_dir/build/bin/Release/ next to llama-server.exe. * #5324 prepends pip-installed nvidia//{bin,bin/x86_64,Library/ bin} and torch/lib to PATH when launching llama-server.exe. CI has no GPU so nvidia-smi is mocked; everything else (resolver, PATH builder, install layout) runs against a real filesystem. """ from __future__ import annotations import os import subprocess import sys import types as _types import zipfile from pathlib import Path from unittest import mock import pytest _BACKEND_DIR = str(Path(__file__).resolve().parent.parent) if _BACKEND_DIR not in sys.path: sys.path.insert(0, _BACKEND_DIR) # Stub heavy deps only if they fail to import (unconditional stubs would shadow # the real module for sibling tests). Use try-import, not find_spec: loggers # imports fastapi at load, so find_spec succeeds but the import then raises. import importlib as _importlib # noqa: E402 def _maybe_stub(name: str, builder): try: _importlib.import_module(name) except ImportError: sys.modules[name] = builder() def _build_loggers_stub(): m = _types.ModuleType("loggers") m.get_logger = lambda name: __import__("logging").getLogger(name) return m def _build_structlog_stub(): return _types.ModuleType("structlog") def _build_httpx_stub(): m = _types.ModuleType("httpx") for _exc_name in ( "ConnectError", "TimeoutException", "ReadTimeout", "ReadError", "RemoteProtocolError", "CloseError", "HTTPError", ): setattr(m, _exc_name, type(_exc_name, (Exception,), {})) m.Response = type("Response", (), {}) class _FakeTimeout: def __init__(self, *a, **kw): pass m.Timeout = _FakeTimeout m.Client = type( "Client", (), { "__init__": lambda self, **kw: None, "__enter__": lambda self: self, "__exit__": lambda self, *a: None, }, ) return m _maybe_stub("loggers", _build_loggers_stub) _maybe_stub("structlog", _build_structlog_stub) _maybe_stub("httpx", _build_httpx_stub) from core.inference.llama_cpp import LlamaCppBackend # noqa: E402 # Upstream b9103 cudart bundle: exactly these three DLLs per CUDA major, # no executables or subdirectories. Verified by direct unzip. REAL_UPSTREAM_CUDART_BUNDLE = { "12.4": ("cudart64_12.dll", "cublas64_12.dll", "cublasLt64_12.dll"), "13.1": ("cudart64_13.dll", "cublas64_13.dll", "cublasLt64_13.dll"), } # PyPI win_amd64 wheel layouts, verified via `pip download ... --platform # win_amd64` + `unzip -l`. Resolver only cares about directory structure. REAL_PIP_NVIDIA_WHEEL_LAYOUTS = { # Legacy cu-suffixed wheels "nvidia/cuda_runtime/bin": ["cudart64_12.dll"], "nvidia/cublas/bin": [ "cublas64_12.dll", "cublasLt64_12.dll", "nvblas64_12.dll", ], "nvidia/cudnn/bin": [ "cudnn64_9.dll", "cudnn_adv64_9.dll", "cudnn_ops64_9.dll", ], # Unsuffixed cu13 wheels "nvidia/cu13/bin/x86_64": [ "cudart64_13.dll", "cublas64_13.dll", "cublasLt64_13.dll", "nvblas64_13.dll", ], } def _populate_studio_venv(prefix: Path) -> None: """Lay out fake nvidia + torch wheels matching real win_amd64 layouts (stub bytes).""" site = prefix / "Lib" / "site-packages" for rel, dlls in REAL_PIP_NVIDIA_WHEEL_LAYOUTS.items(): d = site / Path(rel) d.mkdir(parents = True, exist_ok = True) for name in dlls: (d / name).write_bytes(b"PE-stub") # install_python_stack always installs torch beside nvidia. (site / "torch" / "lib").mkdir(parents = True, exist_ok = True) for fn in ("c10.dll", "torch.dll", "torch_cpu.dll", "torch_python.dll"): (site / "torch" / "lib" / fn).write_bytes(b"PE-stub") def _populate_studio_install(install_dir: Path, runtime: str = "13.1") -> None: """Lay out install_dir/build/bin/Release/ as #5322 leaves it: payload + cudart overlay.""" rel = install_dir / "build" / "bin" / "Release" rel.mkdir(parents = True, exist_ok = True) for fn in ( "llama-server.exe", "llama-quantize.exe", "llama-cli.exe", "llama.dll", "ggml.dll", "ggml-base.dll", "ggml-cuda.dll", "mtmd.dll", ): (rel / fn).write_bytes(b"PE-stub") # The cudart overlay from #5322. for fn in REAL_UPSTREAM_CUDART_BUNDLE[runtime]: (rel / fn).write_bytes(b"PE-stub") def _build_path_dirs_like_start_llama_server( binary_dir: Path, prefix: Path, cuda_path: str = "", ) -> list[str]: """Wrapper around the real _build_windows_path_dirs staticmethod.""" return LlamaCppBackend._build_windows_path_dirs(str(binary_dir), str(prefix), cuda_path) def _mock_nvidia_smi_run(fake_output: str, returncode: int = 0) -> "mock._patch": """Patch subprocess.run so the nvidia-smi probe returns fake_output; other calls pass through.""" real_run = subprocess.run def fake_run(cmd, *args, **kwargs): if isinstance(cmd, list) and cmd and "nvidia-smi" in cmd[0]: return subprocess.CompletedProcess( args = cmd, returncode = returncode, stdout = fake_output, stderr = "" ) return real_run(cmd, *args, **kwargs) return mock.patch("subprocess.run", side_effect = fake_run) # --------------------------------------------------------------------- # # Tests # --------------------------------------------------------------------- # class TestWindowsGpuDetectionAfter5106Fix: """End-to-end #5106 fix on a synthetic Windows layout. nvidia-smi mocked; resolver, PATH builder, and install layout run live.""" def test_nvidia_smi_probe_reports_synthetic_gpu(self, monkeypatch): """Probe parses CSV output and returns (index, free_mib).""" # Clear inherited masks so the synthetic CSV isn't filtered. monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising = False) monkeypatch.delenv("NVIDIA_VISIBLE_DEVICES", raising = False) # The #5106 reporter's exact reproducer: RTX 4090, 22805 MiB. fake_csv = "0, 22805\n" with _mock_nvidia_smi_run(fake_csv): gpus = LlamaCppBackend._get_gpu_free_memory() assert gpus == [(0, 22805)], f"GPU probe failed to parse mocked nvidia-smi output: {gpus}" def test_nvidia_smi_probe_respects_cuda_visible_devices(self, monkeypatch): """CUDA_VISIBLE_DEVICES=1 -> only GPU 1 visible.""" fake_csv = "0, 22805\n1, 24576\n2, 16384\n" monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "1") with _mock_nvidia_smi_run(fake_csv): gpus = LlamaCppBackend._get_gpu_free_memory() assert gpus == [(1, 24576)], gpus def test_get_gpu_memory_parses_three_and_two_column(self, monkeypatch): """Total is parsed when present; a legacy two-column line or a non-integer total ("N/A") yields total 0 (back-compat) rather than dropping the GPU, which would silently spill to CPU.""" monkeypatch.delenv("CUDA_VISIBLE_DEVICES", raising = False) with _mock_nvidia_smi_run("0, 22805, 24576\n"): assert LlamaCppBackend._get_gpu_memory() == [(0, 22805, 24576)] with _mock_nvidia_smi_run("0, 22805\n"): assert LlamaCppBackend._get_gpu_memory() == [(0, 22805, 0)] # A non-integer total must keep the GPU (total 0), not drop it. with _mock_nvidia_smi_run("0, 22805, N/A\n"): assert LlamaCppBackend._get_gpu_memory() == [(0, 22805, 0)] # A bad free still skips that line (free is required). with _mock_nvidia_smi_run("0, N/A, 24576\n1, 22805, 24576\n"): assert LlamaCppBackend._get_gpu_memory() == [(1, 22805, 24576)] def test_windows_install_dir_has_all_three_cudart_dlls(self, tmp_path): """All three bundle DLLs must land in install_dir/build/bin/ Release; any missing one breaks ggml-cuda.dll's PE import chain.""" install = tmp_path / "studio_install" _populate_studio_install(install, runtime = "13.1") rel = install / "build" / "bin" / "Release" for fn in REAL_UPSTREAM_CUDART_BUNDLE["13.1"]: assert (rel / fn).exists(), f"missing {fn} in {rel}" assert (rel / "llama-server.exe").exists() assert (rel / "ggml-cuda.dll").exists() def test_resolver_finds_real_pypi_wheel_layouts(self, tmp_path): """Resolver must pick up every wheel layout: nvidia//bin, nvidia//bin/x86_64, torch/lib.""" prefix = tmp_path / "studio_venv" _populate_studio_venv(prefix) out = LlamaCppBackend._windows_pip_nvidia_dll_dirs(str(prefix)) site = prefix / "Lib" / "site-packages" for expected in ( site / "nvidia" / "cuda_runtime" / "bin", site / "nvidia" / "cublas" / "bin", site / "nvidia" / "cudnn" / "bin", site / "nvidia" / "cu13" / "bin" / "x86_64", site / "torch" / "lib", ): assert str(expected) in out, f"resolver missed {expected.relative_to(prefix)}: {out}" def test_path_assembly_makes_cudart_reachable_without_toolkit(self, tmp_path): """The #5106 scenario: GPU detected, pip nvidia wheels present, no system CUDA toolkit. cudart must be reachable from PATH via BOTH binary_dir (#5322) and a pip nvidia dir (#5324).""" prefix = tmp_path / "studio_venv" install = tmp_path / "studio_install" _populate_studio_venv(prefix) _populate_studio_install(install, runtime = "13.1") binary_dir = install / "build" / "bin" / "Release" path_dirs = _build_path_dirs_like_start_llama_server(binary_dir, prefix, cuda_path = "") # binary_dir first -- Windows DLL search step 1. assert path_dirs[0] == str( binary_dir ), f"binary_dir must be first in PATH; got {path_dirs[0]}" cudart_locations = [] for entry in path_dirs: for cudart_name in ("cudart64_12.dll", "cudart64_13.dll"): if (Path(entry) / cudart_name).exists(): cudart_locations.append((entry, cudart_name)) assert cudart_locations, ( f"cudart unreachable from any PATH entry -- #5106 not fixed.\n" f"PATH entries searched: {path_dirs}" ) # Defence in depth: both fix paths contribute cudart. sources = {Path(e).relative_to(tmp_path).parts[0] for e, _ in cudart_locations} assert "studio_install" in sources, f"#5322's cudart drop not reachable: {cudart_locations}" assert ( "studio_venv" in sources ), f"#5324's pip nvidia dir not contributing cudart: {cudart_locations}" def test_cublas_and_cublasLt_also_reachable(self, tmp_path): """ggml-cuda imports cublas64, which imports cublasLt64. All three must resolve or LoadLibrary returns NULL.""" prefix = tmp_path / "studio_venv" install = tmp_path / "studio_install" _populate_studio_venv(prefix) _populate_studio_install(install, runtime = "13.1") binary_dir = install / "build" / "bin" / "Release" path_dirs = _build_path_dirs_like_start_llama_server(binary_dir, prefix) for required in REAL_UPSTREAM_CUDART_BUNDLE["13.1"]: reachable = any((Path(d) / required).exists() for d in path_dirs) assert reachable, ( f"{required} unreachable from PATH; #5106 not fixed.\n" f"PATH entries: {path_dirs}" ) def test_no_pip_nvidia_wheels_still_works_via_install_dir(self, tmp_path): """No pip nvidia wheels (CPU-only torch / standalone unsloth): cudart still resolves via #5322's binary_dir drop.""" prefix = tmp_path / "bare_venv" prefix.mkdir() install = tmp_path / "studio_install" _populate_studio_install(install, runtime = "13.1") binary_dir = install / "build" / "bin" / "Release" path_dirs = _build_path_dirs_like_start_llama_server(binary_dir, prefix) assert path_dirs == [str(binary_dir)], f"bare venv produced unexpected PATH: {path_dirs}" for required in REAL_UPSTREAM_CUDART_BUNDLE["13.1"]: assert ( binary_dir / required ).exists(), f"{required} missing from binary_dir on bare venv install" def test_no_install_dir_still_works_via_pip_wheels(self, tmp_path): """Pre-#5322 install (binary_dir lacks cudart): #5324's pip wheel dirs on PATH still resolve cudart.""" prefix = tmp_path / "studio_venv" _populate_studio_venv(prefix) install = tmp_path / "studio_install_pre5322" rel = install / "build" / "bin" / "Release" rel.mkdir(parents = True) # Main archive payload only; cudart bundle absent. for fn in ( "llama-server.exe", "llama.dll", "ggml-cuda.dll", "ggml-base.dll", ): (rel / fn).write_bytes(b"PE-stub") path_dirs = _build_path_dirs_like_start_llama_server(rel, prefix) cudart_reachable = any( (Path(d) / "cudart64_12.dll").exists() or (Path(d) / "cudart64_13.dll").exists() for d in path_dirs ) assert cudart_reachable, ( "#5324 pip wheel fallback failed: cudart unreachable from PATH " f"on cudart-less install. PATH entries: {path_dirs}" ) cublas_reachable = any( (Path(d) / "cublas64_12.dll").exists() or (Path(d) / "cublas64_13.dll").exists() for d in path_dirs ) assert cublas_reachable, "cublas unreachable on cudart-less install" def test_pre_pr_scenario_would_have_failed(self, tmp_path): """Negative control: pre-#5322 + pre-#5324 leaves cudart unreachable -- the original failure mode. Confirms the test catches a regression.""" prefix = tmp_path / "studio_venv" _populate_studio_venv(prefix) install = tmp_path / "pre_pr_install" rel = install / "build" / "bin" / "Release" rel.mkdir(parents = True) for fn in ("llama-server.exe", "llama.dll", "ggml-cuda.dll"): (rel / fn).write_bytes(b"PE-stub") # Pre-PR PATH: binary_dir only, no pip nvidia dirs, no toolkit. pre_pr_path_dirs = [str(rel)] cudart_reachable_pre = any( (Path(d) / "cudart64_12.dll").exists() or (Path(d) / "cudart64_13.dll").exists() for d in pre_pr_path_dirs ) assert not cudart_reachable_pre, ( "Test self-check failed: pre-PR scenario unexpectedly had " f"cudart reachable. {pre_pr_path_dirs}" ) class TestWindowsSysPlatformMocked: """Confirm we test the win32 branch in start_llama_server, not the linux fallback. Patches sys.platform and re-runs the branch-selecting helper.""" def test_sys_platform_win32_uses_pip_nvidia_resolver(self, monkeypatch, tmp_path): monkeypatch.setattr(sys, "platform", "win32") prefix = tmp_path / "studio_venv" _populate_studio_venv(prefix) out = LlamaCppBackend._windows_pip_nvidia_dll_dirs(str(prefix)) assert out, f"resolver returned empty under sys.platform=win32: {out}" # cu13 arch dir must be in the output. cu13_arch = prefix / "Lib" / "site-packages" / "nvidia" / "cu13" / "bin" / "x86_64" assert str(cu13_arch) in out