Skip to content

Commit 9404217

Browse files
committed
Resolution for support of MPS on Apple Silicon. R1, R2, R3, and R4 implemented. Installation detection and speech enhanceres for MPS if available and supported
1 parent 851f8fb commit 9404217

5 files changed

Lines changed: 183 additions & 58 deletions

File tree

install.py

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,11 @@ def _get_translate_deps_from_registry() -> list:
749749
return _get_packages_for_step([Extra.TRANSLATE])
750750

751751

752+
def _get_llm_deps_from_registry() -> list:
753+
"""Get LLM server package specs from registry (uvicorn, fastapi, etc.)."""
754+
return _get_packages_for_step([Extra.LLM])
755+
756+
752757
def _get_gui_deps_from_registry() -> list:
753758
"""Get GUI package specs from registry."""
754759
return _get_packages_for_step([Extra.GUI])
@@ -887,7 +892,7 @@ def detect_cuda_version(args) -> str:
887892
args: Parsed command-line arguments
888893
889894
Returns:
890-
"cpu", "cu118", or "cu128"
895+
"cpu", "cu118", "cu128", or "metal" (Apple Silicon)
891896
"""
892897
# Explicit user request
893898
if args.cpu_only:
@@ -902,7 +907,8 @@ def detect_cuda_version(args) -> str:
902907
gpu_info = detect_gpu()
903908
if gpu_info.detected:
904909
log(f"\n GPU detected: {gpu_info.name}")
905-
log(f" Driver: {gpu_info.driver_version[0]}.{gpu_info.driver_version[1]}")
910+
if gpu_info.driver_version:
911+
log(f" Driver: {gpu_info.driver_version[0]}.{gpu_info.driver_version[1]}")
906912
log(f" Selected: {gpu_info.cuda_version or 'CPU'}")
907913
return gpu_info.cuda_version or "cpu"
908914
else:
@@ -1229,12 +1235,21 @@ def main():
12291235
# All subsequent packages that depend on torch will see it as satisfied.
12301236
#
12311237
print_header("Installing PyTorch", "Step 2/6")
1232-
torch_url = get_torch_index_url(cuda_version)
1233-
run_pip(
1234-
executor,
1235-
["install", "torch", "torchaudio", "--index-url", torch_url],
1236-
f"Install PyTorch ({cuda_version})"
1237-
)
1238+
if cuda_version == "metal":
1239+
# Apple Silicon: install from default PyPI (has arm64 wheels with MPS support)
1240+
# Do NOT use --index-url — the CPU index provides x86_64-only builds
1241+
run_pip(
1242+
executor,
1243+
["install", "torch", "torchaudio"],
1244+
"Install PyTorch (Metal/MPS)"
1245+
)
1246+
else:
1247+
torch_url = get_torch_index_url(cuda_version)
1248+
run_pip(
1249+
executor,
1250+
["install", "torch", "torchaudio", "--index-url", torch_url],
1251+
f"Install PyTorch ({cuda_version})"
1252+
)
12381253

12391254
# -------------------------------------------------------------------------
12401255
# Step 3: Install core dependencies
@@ -1309,6 +1324,14 @@ def main():
13091324
if translate_deps:
13101325
run_pip(executor, ["install"] + translate_deps, "Install translation packages")
13111326

1327+
# LLM server framework (from registry) - uvicorn, fastapi, etc.
1328+
# These are needed for the local LLM translation server.
1329+
# Installed unconditionally (lightweight, pure Python) so the server
1330+
# is ready if the user installs llama-cpp-python now or later.
1331+
llm_deps = _get_llm_deps_from_registry()
1332+
if llm_deps:
1333+
run_pip(executor, ["install"] + llm_deps, "Install LLM server packages")
1334+
13121335
# -------------------------------------------------------------------------
13131336
# Local LLM (llama-cpp-python) - Included by Default
13141337
# -------------------------------------------------------------------------
@@ -1472,25 +1495,51 @@ def _install_local_llm(executor: StepExecutor, build_from_source: bool):
14721495
is_intel_mac = (sys.platform == "darwin" and platform_module.machine() != "arm64")
14731496

14741497
if is_apple_silicon:
1475-
# Apple Silicon: build from source with Metal
1476-
log(" Apple Silicon detected - building from source with Metal support.")
1477-
if get_llama_cpp_source_info:
1478-
git_url, backend, cmake_args, env_vars = get_llama_cpp_source_info()
1479-
log(f" Backend: {backend}")
1480-
for key, value in env_vars.items():
1481-
log(f" Setting {key}={value}")
1482-
os.environ[key] = value
1483-
if cmake_args:
1484-
log(f" Setting CMAKE_ARGS={cmake_args}")
1485-
os.environ["CMAKE_ARGS"] = cmake_args
1486-
run_pip(
1487-
executor,
1488-
["install", git_url],
1489-
f"Install llama-cpp-python ({backend})",
1490-
allow_fail=True
1491-
)
1492-
else:
1493-
log(" ERROR: llama_build_utils not available")
1498+
# Apple Silicon: try prebuilt Metal wheel first, then source build
1499+
log(" Apple Silicon detected - checking for prebuilt Metal wheel...")
1500+
1501+
# Strategy 1: Try prebuilt Metal wheel (fast, ~1 min download)
1502+
prebuilt_installed = False
1503+
if get_prebuilt_wheel_url:
1504+
wheel_url, wheel_backend = get_prebuilt_wheel_url(verbose=True)
1505+
if wheel_url:
1506+
log(f" Found prebuilt wheel: {wheel_backend}")
1507+
prebuilt_installed = run_pip(
1508+
executor,
1509+
["install", wheel_url],
1510+
f"Install llama-cpp-python ({wheel_backend})",
1511+
allow_fail=True
1512+
)
1513+
if prebuilt_installed:
1514+
run_pip(
1515+
executor,
1516+
["install", "llama-cpp-python[server]"],
1517+
"Install llama-cpp-python server extras",
1518+
allow_fail=True
1519+
)
1520+
else:
1521+
log(" No prebuilt Metal wheel found.")
1522+
1523+
# Strategy 2: Fall back to source build with Metal (slow, ~10 min)
1524+
if not prebuilt_installed:
1525+
log(" Building from source with Metal support...")
1526+
if get_llama_cpp_source_info:
1527+
git_url, backend, cmake_args, env_vars = get_llama_cpp_source_info()
1528+
log(f" Backend: {backend}")
1529+
for key, value in env_vars.items():
1530+
log(f" Setting {key}={value}")
1531+
os.environ[key] = value
1532+
if cmake_args:
1533+
log(f" Setting CMAKE_ARGS={cmake_args}")
1534+
os.environ["CMAKE_ARGS"] = cmake_args
1535+
run_pip(
1536+
executor,
1537+
["install", git_url],
1538+
f"Install llama-cpp-python ({backend})",
1539+
allow_fail=True
1540+
)
1541+
else:
1542+
log(" ERROR: llama_build_utils not available")
14941543

14951544
elif is_intel_mac:
14961545
# Intel Mac: CPU only

whisperjav/installer/core/detector.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -221,13 +221,13 @@ def detect_gpu() -> GPUInfo:
221221
plat = detect_platform()
222222
if plat == DetectedPlatform.MACOS_SILICON:
223223
return GPUInfo(
224-
detected=False,
225-
name=None,
224+
detected=True, # Apple Silicon HAS a GPU (Metal/MPS)
225+
name="Apple Silicon (Metal)",
226226
driver_version=None,
227-
cuda_version=None,
228-
torch_index=CPU_TORCH_INDEX, # macOS uses CPU/Metal, not CUDA
227+
cuda_version="metal", # Sentinel: install torch from default PyPI (has MPS)
228+
torch_index="", # Empty = no --index-url needed, default PyPI has arm64+MPS wheels
229229
detection_method="platform",
230-
message="Apple Silicon detected - CUDA not supported, using Metal",
230+
message="Apple Silicon detected - using Metal/MPS acceleration",
231231
)
232232

233233
# Try nvidia-smi first (most reliable)
@@ -499,6 +499,9 @@ def get_torch_index_url(cuda_version: Optional[str] = None) -> str:
499499
if cuda_version == "cpu":
500500
return CPU_TORCH_INDEX
501501

502+
if cuda_version == "metal":
503+
return "" # Apple Silicon: use default PyPI (has arm64+MPS wheels)
504+
502505
# Find in CUDA matrix
503506
for entry in CUDA_DRIVER_MATRIX:
504507
if entry.cuda_version == cuda_version:

whisperjav/modules/speech_enhancement/backends/bs_roformer.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
load_audio_to_array,
2727
create_failed_result,
2828
resample_audio,
29+
resolve_torch_device,
2930
)
3031

3132
logger = logging.getLogger("whisperjav")
@@ -107,17 +108,23 @@ def _ensure_initialized(self) -> bool:
107108

108109
logger.info(f"Loading BS-RoFormer model for stem: {self._model_name}")
109110

110-
# Determine device
111-
device = self._device
112-
if device == "auto":
113-
try:
114-
import torch
115-
device = "cuda" if torch.cuda.is_available() else "cpu"
116-
except ImportError:
117-
device = "cpu"
111+
# Resolve best available device (cuda > mps > cpu)
112+
device = resolve_torch_device(self._device)
118113

119-
# Initialize separator
120-
self._separator = BSRoformer(device=device)
114+
# Try initializing with the selected device
115+
try:
116+
self._separator = BSRoformer(device=device)
117+
except Exception as dev_err:
118+
# If MPS failed (model may not support all ops), fall back to CPU
119+
if device == "mps":
120+
logger.info(
121+
f"BS-RoFormer does not support MPS ({dev_err}), "
122+
"falling back to CPU"
123+
)
124+
device = "cpu"
125+
self._separator = BSRoformer(device=device)
126+
else:
127+
raise
121128

122129
self._initialized = True
123130
logger.info(f"BS-RoFormer loaded successfully on {device}")

whisperjav/modules/speech_enhancement/backends/zipenhancer.py

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
load_audio_to_array,
3535
create_failed_result,
3636
resample_audio,
37+
resolve_torch_device,
3738
)
3839

3940
logger = logging.getLogger("whisperjav")
@@ -154,21 +155,44 @@ def _init_torch(self) -> bool:
154155

155156
logger.info("Loading ZipEnhancer via ModelScope (torch)...")
156157

157-
# Determine device
158-
device = self.device
159-
if device is None:
160-
device = "gpu" if torch.cuda.is_available() else "cpu"
161-
elif device == "cuda":
162-
device = "gpu" # ModelScope uses "gpu" not "cuda"
163-
164-
self._pipeline = pipeline(
165-
Tasks.acoustic_noise_suppression,
166-
model=MODELSCOPE_MODEL_ID,
167-
device=device
168-
)
158+
# Resolve best available device (cuda > mps > cpu)
159+
torch_device = resolve_torch_device(self.device)
160+
161+
# ModelScope uses "gpu" for CUDA, "cpu" for CPU.
162+
# For MPS: try passing "mps" directly — ModelScope >= 1.20 may
163+
# forward it to PyTorch. If it fails, fall back to CPU.
164+
if torch_device == "cuda":
165+
ms_device = "gpu"
166+
elif torch_device == "mps":
167+
ms_device = "mps" # Will be validated below with fallback
168+
else:
169+
ms_device = "cpu"
170+
171+
# Try to create the pipeline with the selected device
172+
try:
173+
self._pipeline = pipeline(
174+
Tasks.acoustic_noise_suppression,
175+
model=MODELSCOPE_MODEL_ID,
176+
device=ms_device
177+
)
178+
except Exception as mps_err:
179+
# If MPS failed (ModelScope may not support it), fall back to CPU
180+
if ms_device == "mps":
181+
logger.info(
182+
f"ModelScope does not support MPS device ({mps_err}), "
183+
"falling back to CPU"
184+
)
185+
ms_device = "cpu"
186+
self._pipeline = pipeline(
187+
Tasks.acoustic_noise_suppression,
188+
model=MODELSCOPE_MODEL_ID,
189+
device=ms_device
190+
)
191+
else:
192+
raise
169193

170194
self._initialized = True
171-
logger.info(f"ZipEnhancer (torch) loaded successfully on {device}")
195+
logger.info(f"ZipEnhancer (torch) loaded successfully on {ms_device}")
172196
return True
173197

174198
except ImportError as e:
@@ -213,13 +237,14 @@ def _init_onnx(self) -> bool:
213237
f"Directory contents: {contents}"
214238
)
215239

216-
# Configure ONNX providers
240+
# Configure ONNX providers (CUDA > CoreML/Apple Silicon > CPU)
241+
available_providers = ort.get_available_providers()
217242
providers = ['CPUExecutionProvider']
218243
if self.device != 'cpu':
219-
# Try CUDA first if available
220-
available_providers = ort.get_available_providers()
221244
if 'CUDAExecutionProvider' in available_providers:
222245
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
246+
elif 'CoreMLExecutionProvider' in available_providers:
247+
providers = ['CoreMLExecutionProvider', 'CPUExecutionProvider']
223248

224249
self._onnx_session = ort.InferenceSession(
225250
onnx_model_path,

whisperjav/modules/speech_enhancement/base.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,47 @@ def is_lightweight(self) -> bool:
206206
...
207207

208208

209+
def resolve_torch_device(requested: Optional[str] = None) -> str:
210+
"""
211+
Resolve the best available torch device for speech enhancement.
212+
213+
Priority: explicit request > CUDA > MPS (Apple Silicon) > CPU.
214+
Validates that the device actually works before returning it.
215+
216+
Args:
217+
requested: Explicitly requested device ("cuda", "mps", "cpu", "auto", or None).
218+
None and "auto" both trigger auto-detection.
219+
220+
Returns:
221+
Device string suitable for torch: "cuda", "mps", or "cpu".
222+
"""
223+
try:
224+
import torch
225+
except ImportError:
226+
return "cpu"
227+
228+
# Explicit request (not auto)
229+
if requested and requested not in (None, "auto"):
230+
if requested == "cuda" and torch.cuda.is_available():
231+
return "cuda"
232+
if requested == "mps":
233+
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
234+
return "mps"
235+
logger.debug("MPS requested but not available, falling back to CPU")
236+
return "cpu"
237+
if requested == "cpu":
238+
return "cpu"
239+
# Unknown device string — fall through to auto-detect
240+
logger.debug(f"Unknown device '{requested}', auto-detecting")
241+
242+
# Auto-detect: CUDA > MPS > CPU
243+
if torch.cuda.is_available():
244+
return "cuda"
245+
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
246+
return "mps"
247+
return "cpu"
248+
249+
209250
def load_audio_to_array(
210251
audio: Union[np.ndarray, Path, str],
211252
target_sample_rate: Optional[int] = None

0 commit comments

Comments
 (0)