Skip to content

Commit 35d95a0

Browse files
authored
Automatically check if CPU is supported for Audio Analysis (#4166)
# What does this implement/fix? Gate incompatible hardware for AA providers. ## Types of changes <!-- Tick exactly one box. CI (.github/workflows/pr-labels.yaml) derives the label from the ticked box and applies it automatically; the release-notes generator uses that same label to slot this change into the next release notes. --> - [ ] Bugfix (non-breaking change which fixes an issue) — `bugfix` - [ ] New feature (non-breaking change which adds functionality) — `new-feature` - [x] Enhancement to an existing feature — `enhancement` - [ ] New music/player/metadata/plugin provider — `new-provider` - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — `breaking-change` - [ ] Refactor (no behaviour change) — `refactor` - [ ] Documentation only — `documentation` - [ ] Maintenance / chore — `maintenance` - [ ] CI / workflow change — `ci` - [ ] Dependencies bump — `dependencies` ## Checklist - [ ] The code change is tested and works locally. - [x] `pre-commit run --all-files` passes. - [x] `pytest` passes, and tests have been added/updated under `tests/` where applicable. - [ ] For changes to shared models, the companion PR in `music-assistant/models` is linked. - [ ] For changes affecting the UI, the companion PR in `music-assistant/frontend` is linked. - [x] I have read and complied with the project's [AI Policy](https://github.com/music-assistant/.github/blob/main/AI_POLICY.md) for any AI-assisted contributions. - [ ] I have raised a PR against the documentation repository targeting the main or beta branch as appropriate.
1 parent 202a02d commit 35d95a0

6 files changed

Lines changed: 100 additions & 1 deletion

File tree

music_assistant/helpers/util.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import chardet
3131
import ifaddr
3232
from music_assistant_models.enums import AlbumType, IdentifierType
33+
from music_assistant_models.errors import SetupFailedError
3334
from zeroconf import InterfaceChoice, IPVersion
3435

3536
from music_assistant.constants import (
@@ -129,6 +130,27 @@ def get_total_system_memory() -> float:
129130
return 0.0
130131

131132

133+
def verify_cpu_supports_ml_inference() -> None:
134+
"""
135+
Verify the CPU can run on-device ML (torch) inference.
136+
137+
:raises SetupFailedError: If this is an x86 CPU without AVX2 support, which
138+
torch's FBGEMM quantized backend requires.
139+
"""
140+
if platform.machine().lower() not in ("x86_64", "amd64", "i386", "i686", "x86"):
141+
# non-x86 (ARM) machines run quantized inference via QNNPACK instead of FBGEMM
142+
return
143+
import torch # noqa: PLC0415
144+
145+
if torch.backends.cpu.get_cpu_capability() in ("DEFAULT", "NO AVX"):
146+
raise SetupFailedError(
147+
"On-device audio analysis requires a CPU with AVX2 support "
148+
"(Intel Haswell / AMD Zen or newer). This CPU does not support AVX2. "
149+
"If you are running in a virtual machine (e.g. Proxmox), changing the "
150+
"CPU type to 'host' may expose AVX2 to the guest."
151+
)
152+
153+
132154
keyword_pattern = re.compile("title=|artist=")
133155
title_pattern = re.compile(r"title=\"(?P<title>.*?)\"")
134156
artist_pattern = re.compile(r"artist=\"(?P<artist>.*?)\"")

music_assistant/providers/smart_fades/provider.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from torchaudio.transforms import SpectralCentroid
1717

1818
from music_assistant.constants import VERBOSE_LOG_LEVEL
19+
from music_assistant.helpers.util import verify_cpu_supports_ml_inference
1920
from music_assistant.models.audio_analysis import AudioAnalysisData
2021
from music_assistant.models.audio_analysis_provider import AudioAnalysisProvider
2122

@@ -73,6 +74,7 @@ def __init__(
7374

7475
async def handle_async_init(self) -> None:
7576
"""Handle async initialization of the provider."""
77+
verify_cpu_supports_ml_inference()
7678
(
7779
self._beat_this_model,
7880
self._beat_this_post_processor,

music_assistant/providers/sonic_analysis/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption
1414
from music_assistant_models.enums import ConfigEntryType, ContentType
1515

16+
from music_assistant.helpers.util import verify_cpu_supports_ml_inference
1617
from music_assistant.models.audio_analysis import AudioAnalysisData
1718
from music_assistant.models.audio_analysis_provider import (
1819
AnalysisSessionData,
@@ -319,6 +320,7 @@ async def handle_async_init(self) -> None:
319320
available=False, which the AudioAnalysisController already honors when
320321
scheduling work.
321322
"""
323+
verify_cpu_supports_ml_inference()
322324
(
323325
self._clap_model,
324326
self._clap_text_embeddings,

tests/core/test_helpers.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import pytest
77
from music_assistant_models.enums import MediaType
8-
from music_assistant_models.errors import MusicAssistantError
8+
from music_assistant_models.errors import MusicAssistantError, SetupFailedError
99
from zeroconf import InterfaceChoice, IPVersion
1010

1111
from music_assistant.helpers import uri, util
@@ -393,3 +393,42 @@ def test_get_zeroconf_args_all_interfaces() -> None:
393393
assert result["ip_version"] == IPVersion.All
394394
assert isinstance(result["interfaces"], list)
395395
assert "192.168.1.10" in result["interfaces"]
396+
397+
398+
@pytest.mark.parametrize(
399+
("machine", "capability", "should_raise"),
400+
[
401+
("x86_64", "DEFAULT", True),
402+
("AMD64", "DEFAULT", True),
403+
("x86_64", "NO AVX", True),
404+
("x86_64", "AVX2", False),
405+
("x86_64", "AVX512", False),
406+
# a future torch capability string we don't know about yet must fail open
407+
("x86_64", "AVX10", False),
408+
],
409+
)
410+
def test_verify_cpu_supports_ml_inference_x86(
411+
machine: str, capability: str, should_raise: bool
412+
) -> None:
413+
"""x86 CPUs require AVX2/AVX512 for torch's FBGEMM quantized inference."""
414+
with (
415+
patch("music_assistant.helpers.util.platform.machine", return_value=machine),
416+
patch("torch.backends.cpu.get_cpu_capability", return_value=capability),
417+
):
418+
if should_raise:
419+
with pytest.raises(SetupFailedError):
420+
util.verify_cpu_supports_ml_inference()
421+
else:
422+
util.verify_cpu_supports_ml_inference()
423+
424+
425+
def test_verify_cpu_supports_ml_inference_arm() -> None:
426+
"""ARM machines pass without consulting torch (QNNPACK backend works there)."""
427+
with (
428+
patch("music_assistant.helpers.util.platform.machine", return_value="aarch64"),
429+
patch(
430+
"torch.backends.cpu.get_cpu_capability",
431+
side_effect=AssertionError("torch must not be consulted on ARM"),
432+
),
433+
):
434+
util.verify_cpu_supports_ml_inference()

tests/providers/smart_fades/test_provider.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import numpy as np
99
import pytest
1010
from music_assistant_models.enums import ContentType, MediaType
11+
from music_assistant_models.errors import SetupFailedError
1112
from music_assistant_models.media_items import AudioFormat
1213

1314
from music_assistant.models.audio_analysis import AudioAnalysisData
@@ -312,3 +313,20 @@ async def test_finalize_returns_none_on_early_exit(provider: SmartFadesProvider)
312313
result = await provider._finalize(session_id)
313314

314315
assert result is None
316+
317+
318+
async def test_handle_async_init_raises_on_unsupported_cpu(
319+
mass_mock: Mock, manifest_mock: Mock, config_mock: Mock
320+
) -> None:
321+
"""Setup fails before model initialization when the CPU lacks AVX2."""
322+
prov = SmartFadesProvider(mass_mock, manifest_mock, config_mock, set())
323+
with (
324+
patch(
325+
"music_assistant.providers.smart_fades.provider.verify_cpu_supports_ml_inference",
326+
side_effect=SetupFailedError("CPU lacks AVX2"),
327+
),
328+
patch.object(SmartFadesProvider, "_initialize_models") as init_models_mock,
329+
pytest.raises(SetupFailedError),
330+
):
331+
await prov.handle_async_init()
332+
init_models_mock.assert_not_called()

tests/providers/sonic_analysis/test_clap_background_load.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import pytest
99
from music_assistant_models.enums import ContentType
10+
from music_assistant_models.errors import SetupFailedError
1011
from music_assistant_models.media_items import AudioFormat
1112

1213
from music_assistant.providers.sonic_analysis import (
@@ -220,3 +221,18 @@ async def test_start_analysis_returns_false_without_duration(
220221

221222
debug_msgs = [str(c) for c in provider.logger.debug.call_args_list] # type: ignore[attr-defined]
222223
assert any("duration missing or zero" in c for c in debug_msgs)
224+
225+
226+
async def test_handle_async_init_raises_on_unsupported_cpu() -> None:
227+
"""Setup fails before any model load when the CPU lacks AVX2."""
228+
provider = _make_provider()
229+
with (
230+
patch(
231+
"music_assistant.providers.sonic_analysis.verify_cpu_supports_ml_inference",
232+
side_effect=SetupFailedError("CPU lacks AVX2"),
233+
),
234+
patch.object(SonicAnalysisProvider, "_load_clap") as load_clap_mock,
235+
pytest.raises(SetupFailedError),
236+
):
237+
await provider.handle_async_init()
238+
load_clap_mock.assert_not_called()

0 commit comments

Comments
 (0)