fix(model): detect CUDA toolkit outside PATH and rank by installed build

cuda_toolkit_ready() only accepted nvcc on PATH, so a session started
before the CUDA profile snippet ranked Vulkan above a CUDA-compiled build.
A complete toolkit is now also recognised under CUDA_HOME, CUDA_PATH or
/usr/local/cuda*, requiring nvcc, include/cuda_runtime.h and libcudart in
the architecture-matching target. The backend order only names backends
the installed build compiled in, so a Vulkan-only build never lists CUDA.

Refs #20
This commit is contained in:
2026-11-06 19:51:57 +05:30
parent 180cd4d512
commit e88afd224f
3 changed files with 242 additions and 12 deletions
+144
View File
@@ -8,8 +8,12 @@ protocol headroom. Replacing them with a recorded measurement is a
deliberate change to this test.
"""
import os
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from vocalinux.speech_recognition import model_ladder
from vocalinux.speech_recognition.model_ladder import (
DEFAULT_RUNG,
@@ -130,6 +134,146 @@ class TestCudaToolkitReadiness:
assert cuda_toolkit_ready_from_probe(True, True, None) is False
NVCC_VERSION_OUTPUT = "Cuda compilation tools, release 13.2, V13.2.51\n"
NVIDIA_SMI_OUTPUT = "3711, 8.6\n"
def _write_toolkit(
root: Path,
*,
nvcc: bool = True,
header: bool = True,
libcudart: bool = True,
target: str = "x86_64-linux",
) -> Path:
"""Lay out a CUDA toolkit tree shaped like the NVIDIA runfile install."""
if nvcc:
(root / "bin").mkdir(parents=True, exist_ok=True)
binary = root / "bin" / "nvcc"
binary.write_text("#!/bin/sh\n")
binary.chmod(0o755)
if header:
(root / "include").mkdir(parents=True, exist_ok=True)
(root / "include" / "cuda_runtime.h").write_text("/* header */\n")
if libcudart:
lib = root / "targets" / target / "lib"
lib.mkdir(parents=True, exist_ok=True)
(lib / "libcudart.so.13").write_text("")
if target == "x86_64-linux":
(root / "lib64").symlink_to(lib, target_is_directory=True)
return root
def _probe_stub(nvcc_output: str | None):
"""Stand in for ``_run_probe``: nvidia-smi sees an RTX-class GPU."""
def probe(argv, timeout=10):
if argv[0] == "nvidia-smi":
return NVIDIA_SMI_OUTPUT
if argv[0] == "nvcc":
return nvcc_output
return None
return probe
@pytest.fixture
def no_env_toolkit(tmp_path: Path):
"""Isolate detection from the host: no CUDA env vars, empty /usr/local."""
usr_local = tmp_path / "usr-local"
usr_local.mkdir()
with (
patch.dict(os.environ, {}, clear=True),
patch.object(model_ladder, "_CUDA_USR_LOCAL_ROOT", usr_local),
patch.object(model_ladder.platform, "machine", return_value="x86_64"),
):
yield usr_local
class TestCudaToolkitDetection:
"""A toolkit counts from PATH, CUDA_HOME, CUDA_PATH or /usr/local/cuda*/."""
def test_toolkit_found_via_path(self, no_env_toolkit: Path) -> None:
with patch.object(model_ladder, "_run_probe", side_effect=_probe_stub(NVCC_VERSION_OUTPUT)):
assert model_ladder.cuda_toolkit_ready() is True
@pytest.mark.parametrize("variable", ["CUDA_HOME", "CUDA_PATH"])
def test_toolkit_found_via_cuda_env_root(
self, tmp_path: Path, no_env_toolkit: Path, variable: str
) -> None:
root = _write_toolkit(tmp_path / "opt" / "cuda")
with (
patch.dict(os.environ, {variable: str(root)}, clear=True),
patch.object(model_ladder, "_run_probe", side_effect=_probe_stub(None)),
):
assert model_ladder.cuda_toolkit_ready() is True
def test_toolkit_found_only_under_usr_local_cuda(self, no_env_toolkit: Path) -> None:
"""The real target: /usr/local/cuda -> cuda-13.2 with nvcc off PATH."""
real = _write_toolkit(no_env_toolkit / "cuda-13.2")
(no_env_toolkit / "cuda").symlink_to(real, target_is_directory=True)
with patch.object(model_ladder, "_run_probe", side_effect=_probe_stub(None)):
assert model_ladder.cuda_toolkit_ready() is True
def test_no_toolkit_anywhere_is_not_ready(self, no_env_toolkit: Path) -> None:
with patch.object(model_ladder, "_run_probe", side_effect=_probe_stub(None)):
assert model_ladder.cuda_toolkit_ready() is False
@pytest.mark.parametrize(
"layout",
[
{"nvcc": False},
{"header": False},
{"libcudart": False},
{"target": "sbsa-linux"},
],
ids=["no-nvcc", "no-cuda-runtime-header", "no-libcudart", "other-architecture"],
)
def test_incomplete_toolkit_is_rejected(
self, tmp_path: Path, no_env_toolkit: Path, layout: dict
) -> None:
root = _write_toolkit(tmp_path / "partial", **layout)
with (
patch.dict(os.environ, {"CUDA_HOME": str(root)}, clear=True),
patch.object(model_ladder, "_run_probe", side_effect=_probe_stub(None)),
):
assert model_ladder.cuda_toolkit_ready() is False
def test_driver_without_toolkit_stays_not_ready_with_partial_tree(
self, no_env_toolkit: Path
) -> None:
"""A /usr/local/cuda with headers only is not a toolkit."""
_write_toolkit(no_env_toolkit / "cuda-13.2", nvcc=False, libcudart=False)
with patch.object(model_ladder, "_run_probe", side_effect=_probe_stub(None)):
assert model_ladder.cuda_toolkit_ready() is False
class TestBuildAwareBackendOrder:
"""The order names only backends the installed build compiled in."""
def test_vulkan_only_build_never_picks_cuda_even_with_toolkit(
self, no_env_toolkit: Path
) -> None:
_write_toolkit(no_env_toolkit / "cuda-13.2")
(no_env_toolkit / "cuda").symlink_to(no_env_toolkit / "cuda-13.2")
with patch.object(model_ladder, "_run_probe", side_effect=_probe_stub(None)):
assert model_ladder.cuda_toolkit_ready() is True
assert preferred_backend_order("vulkan") == ("vulkan", "cpu")
def test_cuda_build_picks_cuda_with_nvcc_off_path(self, no_env_toolkit: Path) -> None:
_write_toolkit(no_env_toolkit / "cuda-13.2")
(no_env_toolkit / "cuda").symlink_to(no_env_toolkit / "cuda-13.2")
with patch.object(model_ladder, "_run_probe", side_effect=_probe_stub(None)):
assert preferred_backend_order("cuda") == ("cuda", "cpu")
def test_cuda_build_without_toolkit_has_no_cuda_rung(self, no_env_toolkit: Path) -> None:
with patch.object(model_ladder, "_run_probe", side_effect=_probe_stub(None)):
assert preferred_backend_order("cuda") == ("cpu",)
def test_cpu_build_is_cpu_only(self) -> None:
assert preferred_backend_order("cpu") == ("cpu",)
class TestPreferredBackendOrder:
"""CUDA ranks first only with a complete toolkit."""