* fix(settings): contain + tighten the whole Settings surface (measure cap, container-query stacking, wrap the shared rows) Two systemic issues drove 'too spread out' + 'elements go out of view' across many Settings pages: 1. Spread — .settings-content capped at 1280px, so on wide windows every label-left/control-right row left a huge void. Introduce a --settings-measure token (720px, macOS-like) + --settings-rail, and cap the content to it, left-aligned under the nav. One token now controls the reading width. 2. Overflow + bad responsiveness — the row stack break was a *viewport* media query (560px), but the 168px nav rail means a 760px-viewport window only has ~530px of content, so rows went side-by-side in a cramped box. Make .settings-content a container (container-type: inline-size) and stack on the CONTENT width via @container, keeping the viewport @media as a fallback for the .st-row instances used outside Settings (Splash/FirstRun/Dub/SetupWizard). 3. The shared .perfpanel__row (button/badge row reused by 6+ panels: RemoteBackend, HFMirror, LLMEndpoint, Pronunciation, MCPBindings, …) was an inline-flex with no wrap and no max-width, so it ran off the right edge — add flex-wrap + max-width:100% + min-width:0. Plus two rigid-width fixes that escaped the row cap: ApiKeys input min-width:220→0, Appearance scale floor. Frontend builds clean; tokens, @container query, and the wrap all verified in the emitted CSS bundle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(settings): center the settings block + tighten measure (kill the lopsided right void) The capped content was left-aligned, so on a wide window everything jammed to the left with a dead empty third on the right (screenshot). Center the whole settings block (nav rail + content) as a unit via max-width + margin-inline:auto, and drop the measure 720→660 so label→control rows read denser. The cap is computed from the tokens (rail + gap + measure + page padding) so the content track lands exactly at --settings-measure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(device): fall back to CPU when the GPU arch is unsupported, instead of 500-ing every generate (#756) get_best_device() called check_device_compatibility() and, on an unsupported compute capability, only LOGGED a warning then still returned 'cuda' — so the model loaded on a GPU whose kernels can't launch and every generate 500'd with 'CUDA error: no kernel image is available for execution'. Both a too-old card (Pascal sm_61, GTX 10-series) and a too-new one (Blackwell sm_120 on pre-cu128 wheels) hit this. Now an unsupported arch falls back to CPU (works, just slower) with a clear warning; OMNIVOICE_FORCE_CUDA=1 overrides. Belt-and-suspenders: _oom_friendly_reraise classifies a raw 'no kernel image is available' as an unsupported-GPU error (switch to CPU / install matching torch) rather than the OOM/Flush message. Tests: get_best_device → cpu on incompatible, stays cuda on compatible, honors the force override; reraise gives the actionable GPU message, not OOM. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(device): patch detect_host_caps via string path so the #756 fallback test is full-suite robust The first version aliased the import + inserted backend on sys.path, which patched a module copy get_best_device's local 'from core.device_caps import detect_host_caps' didn't resolve in the full suite (passed alone, failed in CI). Use the string-form monkeypatch target; verified passing alongside the other device/model tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): fold #757 device-fallback entry into [0.3.8]; drop the merge's stale [Unreleased] dupe --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
47 lines
1.8 KiB
Python
47 lines
1.8 KiB
Python
"""#756: a GPU whose compute capability isn't in the installed PyTorch build's
|
|
arch list can't launch CUDA kernels ("no kernel image is available for
|
|
execution"), so every generate 500s. get_best_device() must fall back to CPU so
|
|
the app still works (slowly) instead of dead-ending — unless the user explicitly
|
|
forces CUDA. These tests pin that fallback (and the override) without a GPU.
|
|
"""
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
import services.model_manager as mm
|
|
|
|
|
|
@pytest.fixture
|
|
def cuda_host(monkeypatch):
|
|
# Pretend a CUDA GPU is present. Patch detect_host_caps via its string path so
|
|
# the lookup resolves the same module object get_best_device imports locally
|
|
# (`from core.device_caps import detect_host_caps`) — patching an aliased
|
|
# import can miss that in a full-suite run.
|
|
monkeypatch.setattr(
|
|
"core.device_caps.detect_host_caps", lambda: SimpleNamespace(family="cuda")
|
|
)
|
|
monkeypatch.setattr(mm, "_lazy_torch", lambda: SimpleNamespace())
|
|
monkeypatch.setattr(mm, "_configure_rocm_if_needed", lambda _torch: None)
|
|
monkeypatch.delenv("OMNIVOICE_FORCE_CUDA", raising=False)
|
|
|
|
|
|
def test_unsupported_gpu_falls_back_to_cpu(cuda_host, monkeypatch):
|
|
monkeypatch.setattr(
|
|
mm, "check_device_compatibility",
|
|
lambda: (False, "GTX 1080 Ti (sm_61) is not supported by this PyTorch build"),
|
|
)
|
|
assert mm.get_best_device() == "cpu"
|
|
|
|
|
|
def test_supported_gpu_stays_on_cuda(cuda_host, monkeypatch):
|
|
monkeypatch.setattr(mm, "check_device_compatibility", lambda: (True, None))
|
|
assert mm.get_best_device() == "cuda"
|
|
|
|
|
|
def test_force_cuda_overrides_the_fallback(cuda_host, monkeypatch):
|
|
monkeypatch.setattr(
|
|
mm, "check_device_compatibility", lambda: (False, "unsupported arch"),
|
|
)
|
|
monkeypatch.setenv("OMNIVOICE_FORCE_CUDA", "1")
|
|
assert mm.get_best_device() == "cuda"
|