fix(desktop): NVIDIA build silently falling back to CPU (#247) - #267
Merged
Conversation
Three independent defects each land the NVIDIA build on CPU with no visible error and no recovery path: 1. The cpu-only marker was trusted in the shared per-user data dir, not just the app root. The CPU build wrote/migrated that marker there, so anyone who ever ran the CPU build got the NVIDIA build permanently pinned to CPU -- GPU detection never even ran. is_cpu_only_package now checks the app root only; a stale data-dir marker is auto-deleted and logged. 2. A CPU result from a transient failure (no GPU detected, CUDA verify failed) was persisted the same as a real CPU-only package, and the setup gate treated any truthy torchDevice as "done" -- one bad first run pinned CPU forever. Device selection now persists a reason (torchDeviceReason), and the setup gate only treats cuda/mps or a genuine cpu-only package as settled; a failure-born CPU or a legacy install with no reason re-probes the GPU on the next launch. Existing affected installs self-heal on relaunch, no user action needed. 3. nvidia-smi discovery only checked System32 and PATH; some DCH driver installs place it only under DriverStore\FileRepository\nv*\. Added that scan (newest package wins) and raised the first probe's timeout to 30s for Optimus laptops waking a sleeping dGPU. Every detection decision is now logged to setup.log. Also drops the Windows CPU-only portable package's data\cpu-only staging (scripts/windows/make-portable.ps1), which was the source of the poisoned marker. 5 new Rust unit tests cover marker precedence, the self-heal + log line, CPU builds not churning their own marker, and the DriverStore newest-wins scan.
thcp
marked this pull request as ready for review
July 14, 2026 14:21
Companion to the desktop #247 fix, for the server/Docker/Unraid path: device selection was a frozen constant (DEMUCS_DEVICE, computed once at import), so the only override was the STEMDECK_DEMUCS_DEVICE env var plus a restart -- invisible to Docker/Unraid users without container access. - app/core/settings.py: demucs_device setting (auto | cuda | mps | cpu, default auto = hardware probe). Forcing cuda/mps verifies availability BEFORE persisting and rejects with a clear error otherwise -- never persist a device that would silently fall back later (the #247 lesson applied here). STEMDECK_DEMUCS_DEVICE seeds the default so existing env-based deployments keep their forced device. - app/core/config.py: _detect_device -> detect_torch_device (pure hardware probe; env handling moved to the settings seed); DEMUCS_DEVICE constant removed. - app/pipeline/separate.py: reads the device fresh per job -- a Settings change applies to the next separation, no restart. - app/main.py: /api/settings gains demucs_device (choice) and demucs_device_resolved (what jobs will run on); POST validates via the setter (422 with the reason). Startup log and /api/health read live. - static/js/catalog.js: "Compute device" select in Settings -> Advanced, showing the resolved device; a rejected force surfaces the server's reason via showError and reverts the select. Also aligns the port-input fallback with the 8000 default from the earlier port unification. - .docs/improvements/self-hosted-compute-device-setting.md: design doc. 5 new tests: auto-resolution, env seeding, verify-before-persist rejection, unknown-choice rejection, and the API round trip incl. 422 paths.
The Compute device dropdown now disables options that aren't available or detected (Auto and CPU are always selectable; CUDA/MPS depend on the hardware + torch build), labeling them "— not available" so it's clear why. - config.py: available_torch_devices() returns the usable devices best-first; detect_torch_device() is now its first element (no duplicated torch probe). - settings.py: set_demucs_device verifies against membership in available_torch_devices() rather than only the top pick. - /api/settings: new demucs_devices_available list for the UI. - catalog.js: disable + relabel unavailable <option>s on load and after each change.
The Advanced settings pane scrolls, and its scrollbar drew directly over the right-aligned Port / Compute device controls. Reserve a scrollbar gutter (padding-right + equal negative margin so it sits in the card's existing 12px padding), keeping content aligned with the fixed header/footer. Surfaced once the new Compute device row made the pane tall enough to scroll.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #247.
Verdict: still valid
Nothing has touched GPU selection since the issue was filed (only FFmpeg/Unraid commits since 2026-07-04). Code inspection confirmed three concrete, independent defects -- stronger than the issue's own hypotheses.
Root causes
1. Cross-install poisoning (primary).
is_cpu_only_packagetrusted acpu-onlymarker in the shared per-user data dir, not just the app root. The CPU build wrote that marker there on everyensure_workspace, and the legacy migration copied it too. Result: run the CPU build once, then install the NVIDIA build -> GPU detection never runs, permanent silent CPU. Explains "works on some NVIDIA PCs but not others."2. One bad first run pins CPU forever. A CPU result from a transient failure (no GPU detected, CUDA verify failed) was persisted identically to a real CPU-only package, and the setup gate treated any truthy
torchDeviceas "done" -- never re-probing.3. Fragile nvidia-smi discovery. Only checked System32 and PATH; some DCH driver installs place it only under
DriverStore\FileRepository\nv*\. 10s timeout also tight for Optimus laptops waking a sleeping dGPU.Fix
is_cpu_only_package: app root only. Stale data-dir marker is auto-deleted and logged (self-heals a poisoned install).torchDeviceReasonalongside the device; the setup gate (setup.js) only treatscuda/mps, orcpuon a genuine cpu-only package, as settled. A failure-born CPU or a legacy reason-less install re-probes the GPU on the next launch -- existing affected installs self-heal just by relaunching, no user action needed.nvidia_smi_exe: scanDriverStore\FileRepository\nv*\(newest package wins) before falling back to PATH; first probe timeout 10s -> 30s.setup.log(today a silent CPU leaves zero trace).data\cpu-only(the source of the poisoned marker).Deliberately out of scope
Verify
cargo test(desktop/src-tauri): 16/16 pass, including 5 new tests (marker precedence, self-heal + setup.log line, CPU build doesn't churn its own marker, DriverStore newest-wins scan, missing-dir handling).cargo fmt --check: clean.node --check desktop/ui/setup.js: clean.cpu-onlyin%LOCALAPPDATA%\StemDeck) -> launch NVIDIA build -> marker auto-removed (logged), GPU probed, CUDA enabled."torchDevice": "cpu", no reason, in config.json) -> relaunch -> GPU step re-runs and heals tocuda.Follow-up
After merge + a release, comment on #247 explaining the three causes and the self-heal (relaunch fixes pinned installs), and ask the reporter to confirm before closing further.
Added in this PR: compute device selector for the self-hosted server
Companion to the desktop fix above, covering the server/Docker/Unraid path (which is a completely separate code path and was NOT touched by the desktop fix):
app/pipeline/separate.py) -- changes apply to the next separation, no restart. Previously the device was a frozen constant readable only via theSTEMDECK_DEMUCS_DEVICEenv var + restart.STEMDECK_DEMUCS_DEVICEstill seeds the default, so existing env-based deployments are unaffected./api/settingsgainsdemucs_device(user choice) +demucs_device_resolved(what jobs run on);/api/health'sdemucs_devicenow reflects the live value.Tests: 5 new (auto-resolution, env seeding, verify-before-persist rejection, unknown choice, API round trip + 422s) -- 148 total pass. ruff clean,
node --checkclean.