fix(models): recommend the ladder rung from the installed build and free memory
get_recommended_model() now keys to the backend the installed whisper.cpp build actually has (detect_pywhispercpp_gpu_backend) and to free memory measured at recommendation time: free VRAM on a discrete GPU for a Vulkan or CUDA build, free system RAM otherwise. The result is a rung of the model ladder, so a Vulkan-capable Nvidia card reaches the ladder instead of the total-memory tiers, and unquantized turbo, the large family and medium are never a default. Manual selections are untouched. The informational hardware probe no longer decides the model. Dead VRAM-total parsing is removed. free_memory_for_backend() holds the source choice; the floor-rung map is shared by rung selection and step-down.
This commit is contained in:
@@ -53,6 +53,16 @@ PREFER_Q5_0_ABOVE_MIB: float = _MEASURED_Q5_0_PEAK_MIB + HEADROOM_MIB
|
||||
MIN_CUDA_COMPUTE_CAPABILITY: tuple[int, int] = (7, 0)
|
||||
|
||||
|
||||
#: Backends whose kernels run on a GPU, so their model must fit free VRAM on a
|
||||
#: discrete card. A CPU build keeps its model in system RAM.
|
||||
GPU_BACKENDS: tuple[str, ...] = ("vulkan", "cuda")
|
||||
|
||||
|
||||
def _floor_rung(language_is_english: bool) -> str:
|
||||
"""Return the floor rung for the language: English-only pairs with ``.en``."""
|
||||
return FLOOR_RUNG_ENGLISH if language_is_english else FLOOR_RUNG_MULTILINGUAL
|
||||
|
||||
|
||||
def select_ladder_rung(language_is_english: bool, free_mib: float) -> str:
|
||||
"""Return the ladder rung that fits ``free_mib`` MiB of free memory.
|
||||
|
||||
@@ -62,7 +72,7 @@ def select_ladder_rung(language_is_english: bool, free_mib: float) -> str:
|
||||
return DEFAULT_RUNG
|
||||
if free_mib >= PREFER_Q5_0_ABOVE_MIB:
|
||||
return LOW_MEMORY_RUNG
|
||||
return FLOOR_RUNG_ENGLISH if language_is_english else FLOOR_RUNG_MULTILINGUAL
|
||||
return _floor_rung(language_is_english)
|
||||
|
||||
|
||||
def resolve_startup_rung(
|
||||
@@ -89,7 +99,7 @@ def next_rung_down(rung: str, language_is_english: bool) -> str | None:
|
||||
if rung == DEFAULT_RUNG:
|
||||
return LOW_MEMORY_RUNG
|
||||
if rung == LOW_MEMORY_RUNG:
|
||||
return FLOOR_RUNG_ENGLISH if language_is_english else FLOOR_RUNG_MULTILINGUAL
|
||||
return _floor_rung(language_is_english)
|
||||
return None
|
||||
|
||||
|
||||
@@ -197,6 +207,21 @@ def free_system_ram_mib() -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def free_memory_for_backend(gpu_backend: str) -> tuple[float | None, str]:
|
||||
"""Return ``(free MiB, source)`` for the memory the installed build uses.
|
||||
|
||||
``gpu_backend`` is what the installed whisper.cpp build actually has
|
||||
(``vulkan``, ``cuda`` or ``cpu``). A GPU build reads free VRAM on a
|
||||
discrete NVIDIA card; integrated graphics and CPU builds read free system
|
||||
RAM. ``source`` is ``"VRAM"`` or ``"RAM"`` so callers can say which.
|
||||
"""
|
||||
if gpu_backend in GPU_BACKENDS:
|
||||
vram = free_vram_mib()
|
||||
if vram is not None:
|
||||
return vram, "VRAM"
|
||||
return free_system_ram_mib(), "RAM"
|
||||
|
||||
|
||||
def free_memory_mib() -> float | None:
|
||||
"""Return free memory in MiB: discrete-GPU VRAM when present, else RAM."""
|
||||
vram = free_vram_mib()
|
||||
@@ -242,12 +267,14 @@ __all__ = [
|
||||
"PREFER_Q8_0_ABOVE_MIB",
|
||||
"PREFER_Q5_0_ABOVE_MIB",
|
||||
"MIN_CUDA_COMPUTE_CAPABILITY",
|
||||
"GPU_BACKENDS",
|
||||
"select_ladder_rung",
|
||||
"resolve_startup_rung",
|
||||
"next_rung_down",
|
||||
"is_ladder_rung",
|
||||
"cuda_toolkit_ready_from_probe",
|
||||
"cuda_toolkit_ready",
|
||||
"free_memory_for_backend",
|
||||
"free_memory_mib",
|
||||
"preferred_backend_order",
|
||||
"describe_environment",
|
||||
|
||||
@@ -7,7 +7,6 @@ for whisper.cpp, supporting Vulkan, CUDA, and CPU backends.
|
||||
|
||||
import ctypes
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
@@ -541,75 +540,37 @@ def detect_cpu_info() -> str:
|
||||
return "CPU"
|
||||
|
||||
|
||||
def _parse_cuda_vram_gib(backend_info: str) -> Optional[float]:
|
||||
"""Parse VRAM in GiB from nvidia-smi-style backend info.
|
||||
|
||||
Accepts MiB/MB/GiB/GB (case-insensitive). MiB and MB are converted by
|
||||
dividing by 1024. Returns None when no size can be parsed.
|
||||
"""
|
||||
if not isinstance(backend_info, str) or not backend_info:
|
||||
return None
|
||||
match = re.search(r"(\d+(?:\.\d+)?)\s*(gi?b|mi?b)", backend_info, flags=re.IGNORECASE)
|
||||
if not match:
|
||||
return None
|
||||
amount = float(match.group(1))
|
||||
unit = match.group(2).lower()
|
||||
if unit in {"mb", "mib"}:
|
||||
amount /= 1024.0
|
||||
return amount
|
||||
|
||||
|
||||
def get_recommended_model() -> tuple[str, str]:
|
||||
"""
|
||||
Get the recommended whisper.cpp model based on system configuration.
|
||||
"""Recommend a whisper.cpp model from the installed build and free memory.
|
||||
|
||||
Spec #16 / ticket #20: the recommendation is a rung of the model ladder
|
||||
chosen from memory measured now, never from total memory. The memory that
|
||||
counts follows the backend the installed build actually has: free VRAM on a
|
||||
discrete GPU for a Vulkan or CUDA build, free system RAM otherwise (CPU
|
||||
builds, or integrated graphics with no NVIDIA reading). An informational
|
||||
hardware probe no longer decides the model, so a Vulkan-capable Nvidia card
|
||||
reaches the ladder.
|
||||
|
||||
Returns the rung before language adaptation; the floor is returned as the
|
||||
multilingual variant and callers pair it with ``.en`` for English.
|
||||
|
||||
Returns:
|
||||
Tuple of (model_name, reason)
|
||||
"""
|
||||
try:
|
||||
import psutil
|
||||
from ..speech_recognition.model_ladder import (
|
||||
FLOOR_RUNG_MULTILINGUAL,
|
||||
free_memory_for_backend,
|
||||
resolve_startup_rung,
|
||||
)
|
||||
from ..speech_recognition.recognition_manager import detect_pywhispercpp_gpu_backend
|
||||
|
||||
ram_gb = math.ceil(psutil.virtual_memory().total / (1024**3))
|
||||
|
||||
# Detect available compute backends
|
||||
backend, backend_info = detect_compute_backend()
|
||||
|
||||
if backend == ComputeBackend.VULKAN:
|
||||
# Vulkan can handle larger models efficiently
|
||||
if ram_gb >= 8:
|
||||
return "small", f"Vulkan GPU with {ram_gb}GB RAM"
|
||||
else:
|
||||
return "base", f"Vulkan GPU with {ram_gb}GB RAM"
|
||||
elif backend == ComputeBackend.CUDA:
|
||||
# A driver without the complete toolkit cannot run the CUDA
|
||||
# build, so the VRAM ladder below would be unreachable. Fall
|
||||
# through to the Vulkan/CPU branches instead (ticket #20).
|
||||
from ..speech_recognition.model_ladder import cuda_toolkit_ready
|
||||
|
||||
if cuda_toolkit_ready():
|
||||
vram_gb = _parse_cuda_vram_gib(backend_info or "")
|
||||
if vram_gb is not None:
|
||||
if vram_gb >= 8:
|
||||
return "medium", f"CUDA GPU with {vram_gb:g}GB VRAM"
|
||||
if vram_gb >= 4:
|
||||
return "small", f"CUDA GPU with {vram_gb:g}GB VRAM"
|
||||
return "base", "CUDA GPU with limited VRAM"
|
||||
return "small", "CUDA GPU detected"
|
||||
backend = ComputeBackend.CPU
|
||||
if backend == ComputeBackend.CPU:
|
||||
# CPU-only recommendations based on RAM
|
||||
if ram_gb >= 16:
|
||||
return "base", f"{ram_gb}GB RAM - CPU inference"
|
||||
elif ram_gb >= 8:
|
||||
return "tiny", f"{ram_gb}GB RAM - optimized for speed"
|
||||
else:
|
||||
return "tiny", f"Limited RAM ({ram_gb}GB) - fastest model"
|
||||
|
||||
except ImportError:
|
||||
logger.debug("psutil not available for system detection")
|
||||
|
||||
# Default recommendation
|
||||
return "tiny", "Default recommendation"
|
||||
gpu_backend = detect_pywhispercpp_gpu_backend()
|
||||
free_mib, source = free_memory_for_backend(gpu_backend)
|
||||
if free_mib is None:
|
||||
logger.info("Free memory unreadable; recommending the floor model")
|
||||
return FLOOR_RUNG_MULTILINGUAL, "Free memory unreadable; floor model"
|
||||
rung = resolve_startup_rung("", False, free_mib)
|
||||
return rung, f"{rung}: {free_mib:.0f} MiB free {source} ({gpu_backend} build)"
|
||||
|
||||
|
||||
def _model_file_path(model_name: str) -> str:
|
||||
|
||||
+26
-20
@@ -294,27 +294,33 @@ class TestStartupRungSelection:
|
||||
assert manager.get_model_size_for_engine("whisper_cpp") == "medium.en"
|
||||
|
||||
|
||||
class TestRecommendationGate:
|
||||
"""Driver-only CUDA demotes to CPU logic; toolkit keeps the VRAM ladder."""
|
||||
class TestFreeMemoryForBackend:
|
||||
"""The memory reading follows the installed build (spec #16, ticket #20)."""
|
||||
|
||||
def _recommend(self, backend, toolkit):
|
||||
from vocalinux.utils import whispercpp_model_info as info
|
||||
def test_vulkan_build_reads_discrete_vram_when_present(self) -> None:
|
||||
with (
|
||||
patch.object(model_ladder, "free_vram_mib", return_value=2600.0),
|
||||
patch.object(model_ladder, "free_system_ram_mib", return_value=9000.0),
|
||||
):
|
||||
assert model_ladder.free_memory_for_backend("vulkan") == (2600.0, "VRAM")
|
||||
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=16 * (1024**3))
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch.object(info, "detect_compute_backend", return_value=backend):
|
||||
with patch(
|
||||
"vocalinux.speech_recognition.model_ladder.cuda_toolkit_ready",
|
||||
return_value=toolkit,
|
||||
):
|
||||
return info.get_recommended_model()
|
||||
def test_cuda_build_reads_discrete_vram_when_present(self) -> None:
|
||||
with patch.object(model_ladder, "free_vram_mib", return_value=1500.0):
|
||||
assert model_ladder.free_memory_for_backend("cuda") == (1500.0, "VRAM")
|
||||
|
||||
def test_driver_only_cuda_demotes_to_cpu(self) -> None:
|
||||
model, _reason = self._recommend(("cuda", "NVIDIA RTX 2050 (4GB)"), False)
|
||||
assert model == "base"
|
||||
def test_gpu_build_without_an_nvidia_reading_reads_ram(self) -> None:
|
||||
"""Integrated graphics: no discrete VRAM reading, so system RAM decides."""
|
||||
with (
|
||||
patch.object(model_ladder, "free_vram_mib", return_value=None),
|
||||
patch.object(model_ladder, "free_system_ram_mib", return_value=6000.0),
|
||||
):
|
||||
assert model_ladder.free_memory_for_backend("vulkan") == (6000.0, "RAM")
|
||||
|
||||
def test_toolkit_cuda_keeps_vram_ladder(self) -> None:
|
||||
model, reason = self._recommend(("cuda", "NVIDIA RTX 2050 (4GB)"), True)
|
||||
assert model == "small"
|
||||
assert "VRAM" in reason
|
||||
def test_cpu_build_never_reads_vram(self) -> None:
|
||||
vram = MagicMock(return_value=2600.0)
|
||||
with (
|
||||
patch.object(model_ladder, "free_vram_mib", vram),
|
||||
patch.object(model_ladder, "free_system_ram_mib", return_value=1200.0),
|
||||
):
|
||||
assert model_ladder.free_memory_for_backend("cpu") == (1200.0, "RAM")
|
||||
vram.assert_not_called()
|
||||
|
||||
@@ -965,321 +965,104 @@ class TestDetectComputeBackend(unittest.TestCase):
|
||||
|
||||
|
||||
class TestGetRecommendedModel(unittest.TestCase):
|
||||
"""Test cases for get_recommended_model function."""
|
||||
|
||||
def test_get_recommended_model_returns_tuple(self):
|
||||
"""Test that get_recommended_model returns a tuple."""
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=8 * (1024**3))
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend",
|
||||
return_value=("cpu", "Intel Core i7"),
|
||||
):
|
||||
import importlib
|
||||
|
||||
import vocalinux.utils.whispercpp_model_info as mod
|
||||
|
||||
importlib.reload(mod)
|
||||
result = mod.get_recommended_model()
|
||||
self.assertIsInstance(result, tuple)
|
||||
self.assertEqual(len(result), 2)
|
||||
|
||||
def test_get_recommended_model_with_vulkan_high_ram(self):
|
||||
"""Test model recommendation with Vulkan and 8GB+ RAM."""
|
||||
import sys
|
||||
|
||||
# Create a mock psutil module
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=8 * (1024**3))
|
||||
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend"
|
||||
) as mock_backend:
|
||||
mock_backend.return_value = ("vulkan", "Intel Arc A770")
|
||||
|
||||
with patch(
|
||||
"vocalinux.speech_recognition.model_ladder.cuda_toolkit_ready",
|
||||
return_value=True,
|
||||
):
|
||||
from vocalinux.utils.whispercpp_model_info import get_recommended_model
|
||||
|
||||
model, reason = get_recommended_model()
|
||||
|
||||
self.assertEqual(model, "small")
|
||||
|
||||
def test_get_recommended_model_with_cuda_high_vram(self):
|
||||
"""Test model recommendation with CUDA and high VRAM."""
|
||||
import sys
|
||||
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=16 * (1024**3))
|
||||
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend"
|
||||
) as mock_backend:
|
||||
mock_backend.return_value = ("cuda", "NVIDIA RTX 4090 (24GB)")
|
||||
|
||||
with patch(
|
||||
"vocalinux.speech_recognition.model_ladder.cuda_toolkit_ready",
|
||||
return_value=True,
|
||||
):
|
||||
from vocalinux.utils.whispercpp_model_info import get_recommended_model
|
||||
|
||||
model, reason = get_recommended_model()
|
||||
|
||||
# High VRAM should recommend medium or higher
|
||||
self.assertIn(model, ["medium", "large", "small"])
|
||||
|
||||
def test_get_recommended_model_with_driver_only_cuda_demotes(self):
|
||||
"""A CUDA driver without the toolkit cannot run the build: CPU logic."""
|
||||
import sys
|
||||
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=16 * (1024**3))
|
||||
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend"
|
||||
) as mock_backend:
|
||||
mock_backend.return_value = ("cuda", "NVIDIA RTX 4090 (24GB)")
|
||||
|
||||
with patch(
|
||||
"vocalinux.speech_recognition.model_ladder.cuda_toolkit_ready",
|
||||
return_value=False,
|
||||
):
|
||||
from vocalinux.utils.whispercpp_model_info import get_recommended_model
|
||||
|
||||
model, _reason = get_recommended_model()
|
||||
|
||||
# 16 GB system RAM under CPU logic recommends base.
|
||||
self.assertEqual(model, "base")
|
||||
|
||||
def test_get_recommended_model_with_cpu_high_ram(self):
|
||||
"""Test model recommendation with CPU and 16GB+ RAM."""
|
||||
import sys
|
||||
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=16 * (1024**3))
|
||||
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend"
|
||||
) as mock_backend:
|
||||
mock_backend.return_value = ("cpu", "Intel Core i7")
|
||||
|
||||
with patch(
|
||||
"vocalinux.speech_recognition.model_ladder.cuda_toolkit_ready",
|
||||
return_value=True,
|
||||
):
|
||||
from vocalinux.utils.whispercpp_model_info import get_recommended_model
|
||||
|
||||
model, reason = get_recommended_model()
|
||||
|
||||
self.assertEqual(model, "base")
|
||||
|
||||
def test_get_recommended_model_with_cpu_medium_ram(self):
|
||||
"""Test model recommendation with CPU and 8GB RAM."""
|
||||
import sys
|
||||
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=8 * (1024**3))
|
||||
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend"
|
||||
) as mock_backend:
|
||||
mock_backend.return_value = ("cpu", "AMD Ryzen 5")
|
||||
|
||||
with patch(
|
||||
"vocalinux.speech_recognition.model_ladder.cuda_toolkit_ready",
|
||||
return_value=True,
|
||||
):
|
||||
from vocalinux.utils.whispercpp_model_info import get_recommended_model
|
||||
|
||||
model, reason = get_recommended_model()
|
||||
|
||||
self.assertEqual(model, "tiny")
|
||||
|
||||
def test_get_recommended_model_with_cpu_low_ram(self):
|
||||
"""Test model recommendation with CPU and 4GB RAM."""
|
||||
import sys
|
||||
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=4 * (1024**3))
|
||||
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend"
|
||||
) as mock_backend:
|
||||
mock_backend.return_value = ("cpu", "Intel Atom")
|
||||
|
||||
with patch(
|
||||
"vocalinux.speech_recognition.model_ladder.cuda_toolkit_ready",
|
||||
return_value=True,
|
||||
):
|
||||
from vocalinux.utils.whispercpp_model_info import get_recommended_model
|
||||
|
||||
model, reason = get_recommended_model()
|
||||
|
||||
self.assertEqual(model, "tiny")
|
||||
|
||||
def test_get_recommended_model_returns_valid_model_name(self):
|
||||
"""Test that get_recommended_model returns a valid model name."""
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=8 * (1024**3))
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend",
|
||||
return_value=("cpu", "Intel Core i7"),
|
||||
):
|
||||
import importlib
|
||||
|
||||
import vocalinux.utils.whispercpp_model_info as mod
|
||||
|
||||
importlib.reload(mod)
|
||||
model, reason = mod.get_recommended_model()
|
||||
self.assertIn(model, mod.AVAILABLE_MODELS)
|
||||
|
||||
def test_get_recommended_model_returns_reason_string(self):
|
||||
"""Test that get_recommended_model returns a reason string."""
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=8 * (1024**3))
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend",
|
||||
return_value=("cpu", "Intel Core i7"),
|
||||
):
|
||||
import importlib
|
||||
|
||||
import vocalinux.utils.whispercpp_model_info as mod
|
||||
|
||||
importlib.reload(mod)
|
||||
model, reason = mod.get_recommended_model()
|
||||
self.assertIsInstance(reason, str)
|
||||
self.assertGreater(len(reason), 0)
|
||||
|
||||
def test_parse_cuda_vram_mib_and_gb(self):
|
||||
"""nvidia-smi reports MiB; older mocks used GB. Both must parse."""
|
||||
from vocalinux.utils.whispercpp_model_info import _parse_cuda_vram_gib
|
||||
|
||||
self.assertEqual(_parse_cuda_vram_gib("NVIDIA GeForce RTX 3060 (6144 MiB)"), 6.0)
|
||||
self.assertEqual(_parse_cuda_vram_gib("NVIDIA GeForce RTX 3070 (8192 MiB)"), 8.0)
|
||||
self.assertEqual(_parse_cuda_vram_gib("NVIDIA RTX 4090 (24GB)"), 24.0)
|
||||
self.assertEqual(_parse_cuda_vram_gib("NVIDIA RTX 3080 (10 GB)"), 10.0)
|
||||
self.assertEqual(_parse_cuda_vram_gib("NVIDIA RTX 3060 (8 GiB)"), 8.0)
|
||||
self.assertEqual(_parse_cuda_vram_gib("NVIDIA GTX 1650 (4096 MB)"), 4.0)
|
||||
self.assertIsNone(_parse_cuda_vram_gib("NVIDIA GPU"))
|
||||
self.assertIsNone(_parse_cuda_vram_gib(""))
|
||||
|
||||
def test_get_recommended_model_with_cuda_mib_medium(self):
|
||||
"""8 GiB of CUDA VRAM reported as MiB must recommend medium."""
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=16 * (1024**3))
|
||||
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend"
|
||||
) as mock_backend:
|
||||
mock_backend.return_value = ("cuda", "NVIDIA GeForce RTX 3070 (8192 MiB)")
|
||||
|
||||
with patch(
|
||||
"vocalinux.speech_recognition.model_ladder.cuda_toolkit_ready",
|
||||
return_value=True,
|
||||
):
|
||||
from vocalinux.utils.whispercpp_model_info import get_recommended_model
|
||||
|
||||
model, reason = get_recommended_model()
|
||||
|
||||
self.assertEqual(model, "medium")
|
||||
self.assertIn("VRAM", reason)
|
||||
|
||||
def test_get_recommended_model_with_cuda_mib_small(self):
|
||||
"""6 GiB of CUDA VRAM reported as MiB must recommend small."""
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=16 * (1024**3))
|
||||
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend"
|
||||
) as mock_backend:
|
||||
mock_backend.return_value = ("cuda", "NVIDIA GeForce RTX 3060 (6144 MiB)")
|
||||
|
||||
with patch(
|
||||
"vocalinux.speech_recognition.model_ladder.cuda_toolkit_ready",
|
||||
return_value=True,
|
||||
):
|
||||
from vocalinux.utils.whispercpp_model_info import get_recommended_model
|
||||
|
||||
model, reason = get_recommended_model()
|
||||
|
||||
self.assertEqual(model, "small")
|
||||
|
||||
def test_get_recommended_model_with_cuda_mib_base(self):
|
||||
"""Low CUDA VRAM reported as MiB must recommend base."""
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=8 * (1024**3))
|
||||
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend"
|
||||
) as mock_backend:
|
||||
mock_backend.return_value = ("cuda", "NVIDIA GeForce GTX 1050 (2048 MiB)")
|
||||
|
||||
with patch(
|
||||
"vocalinux.speech_recognition.model_ladder.cuda_toolkit_ready",
|
||||
return_value=True,
|
||||
):
|
||||
from vocalinux.utils.whispercpp_model_info import get_recommended_model
|
||||
|
||||
model, reason = get_recommended_model()
|
||||
|
||||
self.assertEqual(model, "base")
|
||||
|
||||
def test_get_recommended_model_with_cuda_unparseable_vram_falls_back(self):
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=16 * (1024**3))
|
||||
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend"
|
||||
) as mock_backend:
|
||||
mock_backend.return_value = ("cuda", "NVIDIA GPU")
|
||||
|
||||
with patch(
|
||||
"vocalinux.speech_recognition.model_ladder.cuda_toolkit_ready",
|
||||
return_value=True,
|
||||
):
|
||||
from vocalinux.utils.whispercpp_model_info import get_recommended_model
|
||||
|
||||
model, reason = get_recommended_model()
|
||||
|
||||
self.assertEqual(model, "small")
|
||||
self.assertIn("CUDA GPU detected", reason)
|
||||
|
||||
def test_get_recommended_model_vulkan_ceils_fractional_ram(self):
|
||||
"""~7.4 GiB must count as 8 GiB so Vulkan recommends small, not base."""
|
||||
mock_psutil = MagicMock()
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(total=int(7.4 * (1024**3)))
|
||||
|
||||
with patch.dict("sys.modules", {"psutil": mock_psutil}):
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend"
|
||||
) as mock_backend:
|
||||
mock_backend.return_value = ("vulkan", "Intel Arc A770")
|
||||
|
||||
with patch(
|
||||
"vocalinux.speech_recognition.model_ladder.cuda_toolkit_ready",
|
||||
return_value=True,
|
||||
):
|
||||
from vocalinux.utils.whispercpp_model_info import get_recommended_model
|
||||
|
||||
model, reason = get_recommended_model()
|
||||
|
||||
self.assertEqual(model, "small")
|
||||
self.assertIn("8GB", reason)
|
||||
"""The recommendation is a ladder rung keyed to the installed build and free memory.
|
||||
|
||||
Spec #16 / ticket #20. Every reading is mocked: the installed build's
|
||||
backend, free VRAM from nvidia-smi and free system RAM. Total memory is
|
||||
never consulted, so no test here passes a total as a free reading.
|
||||
"""
|
||||
|
||||
LADDER = {"large-v3-turbo-q8_0", "large-v3-turbo-q5_0", "small"}
|
||||
|
||||
def _recommend(self, build_backend, free_vram=None, free_ram_mib=10 * 1024.0):
|
||||
from vocalinux.speech_recognition import model_ladder, recognition_manager
|
||||
from vocalinux.utils import whispercpp_model_info as info
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
recognition_manager,
|
||||
"detect_pywhispercpp_gpu_backend",
|
||||
return_value=build_backend,
|
||||
),
|
||||
patch.object(model_ladder, "free_vram_mib", return_value=free_vram),
|
||||
patch.object(model_ladder, "free_system_ram_mib", return_value=free_ram_mib),
|
||||
):
|
||||
return info.get_recommended_model()
|
||||
|
||||
def test_returns_tuple(self):
|
||||
model, reason = self._recommend("vulkan", free_vram=2600.0)
|
||||
self.assertIsInstance(model, str)
|
||||
self.assertIsInstance(reason, str)
|
||||
|
||||
def test_ample_free_vram_on_vulkan_build_selects_default_rung(self):
|
||||
model, reason = self._recommend("vulkan", free_vram=2600.0)
|
||||
self.assertEqual(model, "large-v3-turbo-q8_0")
|
||||
self.assertIn("VRAM", reason)
|
||||
|
||||
def test_constrained_free_vram_steps_to_q5_rung(self):
|
||||
model, reason = self._recommend("vulkan", free_vram=1500.0)
|
||||
self.assertEqual(model, "large-v3-turbo-q5_0")
|
||||
self.assertIn("1500 MiB free VRAM", reason)
|
||||
|
||||
def test_tight_free_vram_selects_floor(self):
|
||||
model, _reason = self._recommend("vulkan", free_vram=900.0)
|
||||
self.assertEqual(model, "small")
|
||||
|
||||
def test_total_vram_is_not_the_reading(self):
|
||||
"""A 4 GB card whose VRAM is mostly taken by the desktop gets the floor."""
|
||||
model, _reason = self._recommend("vulkan", free_vram=512.0)
|
||||
self.assertEqual(model, "small")
|
||||
|
||||
def test_integrated_graphics_read_free_ram(self):
|
||||
"""No NVIDIA reading means the Vulkan build keeps its model in system RAM."""
|
||||
model, reason = self._recommend("vulkan", free_vram=None, free_ram_mib=10 * 1024.0)
|
||||
self.assertEqual(model, "large-v3-turbo-q8_0")
|
||||
self.assertIn("RAM", reason)
|
||||
|
||||
def test_cpu_build_ignores_a_visible_gpu_and_reads_ram(self):
|
||||
"""A CPU-only build never runs on the GPU, so VRAM is not the constraint."""
|
||||
model, reason = self._recommend("cpu", free_vram=2600.0, free_ram_mib=1200.0)
|
||||
self.assertEqual(model, "small")
|
||||
self.assertIn("RAM", reason)
|
||||
self.assertNotIn("VRAM", reason)
|
||||
|
||||
def test_keys_to_the_installed_build_not_the_hardware_probe(self):
|
||||
"""The informational probe reporting CPU must not hide an installed GPU build."""
|
||||
from vocalinux.speech_recognition import model_ladder, recognition_manager
|
||||
from vocalinux.utils import whispercpp_model_info as info
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
info, "detect_compute_backend", return_value=(info.ComputeBackend.CPU, "x")
|
||||
),
|
||||
patch.object(
|
||||
recognition_manager,
|
||||
"detect_pywhispercpp_gpu_backend",
|
||||
return_value="vulkan",
|
||||
),
|
||||
patch.object(model_ladder, "free_vram_mib", return_value=2600.0),
|
||||
patch.object(model_ladder, "free_system_ram_mib", return_value=512.0),
|
||||
):
|
||||
model, reason = info.get_recommended_model()
|
||||
self.assertEqual(model, "large-v3-turbo-q8_0")
|
||||
self.assertIn("VRAM", reason)
|
||||
|
||||
def test_unreadable_memory_recommends_the_floor(self):
|
||||
model, reason = self._recommend("vulkan", free_vram=None, free_ram_mib=None)
|
||||
self.assertEqual(model, "small")
|
||||
self.assertIn("unreadable", reason)
|
||||
|
||||
def test_recommendation_stays_inside_the_ladder_for_any_reading(self):
|
||||
"""Unquantized turbo, the large family and medium never come back as a default."""
|
||||
for backend in ("vulkan", "cuda", "cpu"):
|
||||
for free_vram in (None, 0.0, 900.0, 1500.0, 2600.0, 8000.0):
|
||||
for free_ram in (512.0, 1200.0, 4096.0, 16384.0):
|
||||
model, _reason = self._recommend(backend, free_vram, free_ram)
|
||||
self.assertIn(model, self.LADDER, (backend, free_vram, free_ram))
|
||||
|
||||
def test_reason_names_the_backend_the_build_has(self):
|
||||
_model, reason = self._recommend("cuda", free_vram=2600.0)
|
||||
self.assertIn("cuda build", reason)
|
||||
|
||||
|
||||
class TestGetModelPath(unittest.TestCase):
|
||||
|
||||
Reference in New Issue
Block a user