Skip to content

fix: MLX thread affinity crash + missing mlx-lm/mlx-audio in dev setup - #886

Closed
AlexStephenLytton wants to merge 3 commits into
jamiepine:mainfrom
AlexStephenLytton:fix/mlx-thread-affinity-and-deps
Closed

fix: MLX thread affinity crash + missing mlx-lm/mlx-audio in dev setup#886
AlexStephenLytton wants to merge 3 commits into
jamiepine:mainfrom
AlexStephenLytton:fix/mlx-thread-affinity-and-deps

Conversation

@AlexStephenLytton

@AlexStephenLytton AlexStephenLytton commented Jul 12, 2026

Copy link
Copy Markdown

Summary

Two related bugs on Apple Silicon that turned out to share a root cause, plus one cosmetic health-endpoint fix found while chasing them down.

What's actually going on

Starting from a clean just setup-python on Apple Silicon, MLX never actually gets used:

  1. requirements-mlx.txt only lists mlx + miniaudio. just setup-python installs it and stops — it never runs the pip install --no-deps mlx-lm==0.31.1 / mlx-audio==0.4.1 steps that .github/workflows/release.yml already does for packaged builds. Without mlx_audio importable, get_backend_type() silently falls back to "pytorch" — no error, no log line calling it out, health endpoint still looks plausible. This is just setup-python doesn't install mlx-audio/mlx-lm — MLX backend fails on fresh Apple Silicon dev setup #823.

  2. The obvious fix once you notice just setup-python doesn't install mlx-audio/mlx-lm — MLX backend fails on fresh Apple Silicon dev setup #823 is pip install mlx-lm by hand. That reintroduces a second problem: mlx-lm>=0.30.0 requires transformers>=5.0.0, conflicting with the transformers<=4.57.6 cap in requirements.txt. transformers>=5.0 changed check_model_inputs from a decorator factory (@check_model_inputs()) to a plain decorator (@check_model_inputs, i.e. check_model_inputs(func)). qwen_tts's CustomVoice tokenizer (qwen_tts/core/tokenizer_12hz/modeling_qwen3_tts_tokenizer_v2.py) still calls the old convention, so once transformers gets silently bumped, loading Qwen CustomVoice raises:

    TypeError: check_model_inputs() missing 1 required positional argument: 'func'
    

    --no-deps avoids this entirely (mlx-lm's own transformers requirement is never consulted, so transformers stays at whatever requirements.txt installed) — which is exactly what release.yml already does, it just never made it into justfile.

  3. Once mlx-audio is installed and the backend actually reaches MLX, every model load/generate/transcribe call runs through asyncio.to_thread(), which uses the event loop's default thread pool — no guarantee that two calls land on the same OS thread. MLX's Metal command encoder/stream is thread-local (bound to whichever thread first touched the GPU device), so if model load and a later generate() land on different pool threads, MLX raises:

    There is no Stream(gpu, 1) in current thread.
    

    This is Qwen TTS generation fails 100% with Stream(gpu, 1) not in current thread on Apple Silicon (v0.5.0) #699, reproducible 100% of the time per the original report. Fix: pin every MLX call to one dedicated worker thread instead of the shared pool.

  4. Along the way: /health's backend_variant field never had a branch for backend_type == "mlx", so it always reported "cpu" regardless of whether MLX was actually active — this is what MLX falls back to CPU on Apple M5 Max — GPU not utilized (voicebox 0.5.0) #706 and MLX backend uses CPU variant on Apple Silicon despite gpu_available=true (M5 Pro) #723 point at as evidence MLX wasn't using the GPU. Added the missing branch ("mps"). This is a reporting fix only; it doesn't address whatever's causing MLX-Metal generation to be slower than PyTorch-CPU on the hardware in those two reports specifically — I don't have that hardware to reproduce on, so I'm not claiming this closes them, just that the backend_variant field itself was lying.

Changes

  • justfile: add the two missing pip install --no-deps lines to the Unix setup-python recipe, mirroring release.yml exactly.
  • backend/requirements-mlx.txt: add sounddevice==0.5.3 (mlx-audio's own pin, previously unresolved on a from-scratch install), and a NOTE explaining why mlx-lm/mlx-audio are deliberately not listed here and must go through the --no-deps path instead.
  • backend/backends/mlx_backend.py: dedicated single-worker ThreadPoolExecutor for all MLX calls (_run_on_mlx_thread), replacing the 4 asyncio.to_thread() call sites across MLXTTSBackend and MLXSTTBackend.
  • backend/routes/health.py: 2-line addition — elif backend_type == "mlx": default_variant = "mps".

Testing

All verified on real Apple Silicon hardware (M4):

  • Clean venv, ran just setup-python equivalent commands by hand (matching the justfile diff) → mlx_audio importable, /health reports backend_type: "mlx", backend_variant: "mps" (previously "pytorch" / "cpu").
  • Multiple back-to-back qwen engine generations (model load + several generate() calls across different requests) with no Stream(gpu, N) crash.
  • Qwen CustomVoice 0.6B: pip install mlx-lm==0.31.1 (no --no-deps, reproducing the trap) → confirmed the exact check_model_inputs() TypeError from the report above. Reinstalled per this PR's --no-deps sequence → CustomVoice downloads, loads, and generates successfully.
  • Regression-tested the base qwen MLX engine after the transformers fix to make sure it still works (it does — mlx_audio.tts doesn't touch qwen_tts at all, only the CustomVoice/PyTorch code path does).

Happy to adjust anything — this is my first PR against this repo, let me know if there's a preferred format for splitting these into separate PRs instead of one.

Summary by CodeRabbit

  • Bug Fixes

    • Improved MLX speech-to-text and text-to-speech stability on Apple Silicon by running blocking MLX tasks through a dedicated worker.
    • Prevented unintended network activity during MLX model/cache operations.
    • Fixed health check variant selection to correctly surface the MPS variant for the MLX backend.
    • Expanded device selection to use Apple MPS for PyTorch and Qwen custom voice backends, including clearing MPS cache during unload.
  • Chores

    • Updated Apple Silicon dependency setup by pinning MLX backend packages (including audio) and the required sounddevice runtime.

Two related bugs on Apple Silicon, both stemming from the same root
cause (mlx-lm/mlx-audio never getting installed correctly by `just
setup-python`), plus one cosmetic health-endpoint bug found along the
way:

1. `backend/backends/mlx_backend.py`: pin every MLX model load/generate/
   transcribe call to a single dedicated worker thread instead of
   asyncio.to_thread()'s default pool. MLX's Metal command
   encoder/stream is thread-local — if model load and generation land
   on different pool threads (which asyncio.to_thread() does not
   guarantee against), MLX raises "There is no Stream(gpu, N) in
   current thread." Fixes jamiepine#699.

2. `justfile`: `just setup-python` installed requirements-mlx.txt (mlx
   + miniaudio) on Apple Silicon and stopped — it never ran the
   `pip install --no-deps mlx-lm==0.31.1` / `mlx-audio==0.4.1` steps
   that .github/workflows/release.yml already does for packaged
   builds. Without mlx-audio installed, get_backend_type() falls back
   to "pytorch" silently: no error, no log line calling this out, just
   a backend that's much slower than intended and never actually uses
   MLX. Fixes jamiepine#823.

   Installing mlx-lm without --no-deps (the obvious thing to try once
   you notice jamiepine#823) reintroduces a second problem: mlx-lm>=0.30.0
   requires transformers>=5.0.0, which conflicts with the
   transformers<=4.57.6 cap in requirements.txt. transformers>=5.0
   changed `check_model_inputs` from a decorator factory
   (`@check_model_inputs()`) to a plain decorator
   (`@check_model_inputs`, called as `check_model_inputs(func)`) —
   qwen_tts's CustomVoice tokenizer still uses the old calling
   convention, so loading Qwen CustomVoice raises `TypeError:
   check_model_inputs() missing 1 required positional argument:
   'func'` once transformers gets silently upgraded. --no-deps avoids
   this entirely since mlx-lm's own transformers requirement is never
   consulted; documented this in requirements-mlx.txt so the next
   person doesn't hit the same trap.

3. `backend/routes/health.py`: `/health`'s `backend_variant` field
   never had an `mlx` branch, so it always reported "cpu" regardless
   of whether MLX was active or working — cosmetic, but it's what a
   couple of the linked issues (jamiepine#706, jamiepine#723) used to conclude MLX
   wasn't running on the GPU when `backend_type` said otherwise. Added
   the missing branch so `backend_variant` reports "mps" when the mlx
   backend is selected.

All three verified on real Apple Silicon hardware (M4): repeated
generations with no thread crash, `just setup-python` now installs a
working MLX backend from a clean venv, Qwen CustomVoice downloads and
generates successfully post-fix, and `/health` reports
`backend_variant: "mps"` when MLX is active.

Fixes jamiepine#699, fixes jamiepine#823
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The MLX backend serializes model loading and inference on a dedicated thread, initializes offline Hugging Face caching, installs pinned MLX dependencies during Apple Silicon setup, enables MPS device selection and cache clearing, and reports mps as the MLX health variant.

Changes

MLX reliability and Apple Silicon setup

Layer / File(s) Summary
Thread-pinned MLX execution
backend/backends/mlx_backend.py
MLX TTS and STT model loading, generation, and transcription use a single-worker executor; offline Hugging Face initialization and Qwen config caching occur early.
Apple Silicon dependency provisioning
backend/requirements-mlx.txt, justfile
sounddevice is pinned, and Apple Silicon setup installs pinned mlx-lm and mlx-audio packages with --no-deps.
Apple Silicon device and cache handling
backend/backends/base.py, backend/backends/pytorch_backend.py, backend/backends/qwen_custom_voice_backend.py, backend/routes/health.py
MPS selection and cache clearing are enabled for torch-backed paths, and the MLX health endpoint reports mps as the default variant.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

  • jamiepine/voicebox#482: Updates MLX dependency setup and installs mlx-lm and mlx-audio with --no-deps.
  • jamiepine/voicebox#503: Also modifies MLX backend Hugging Face offline behavior around model loading and inference.

Sequence Diagram(s)

sequenceDiagram
  participant Backend as Backend API
  participant Executor as MLX worker executor
  participant Runtime as MLX TTS/STT runtime
  Backend->>Executor: Submit model load or inference
  Executor->>Runtime: Run MLX operation on dedicated thread
  Runtime-->>Executor: Return model or inference result
  Executor-->>Backend: Return asynchronous result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: MLX thread-affinity fixes and MLX dev setup package installs.
Linked Issues check ✅ Passed The PR addresses #699 by pinning MLX work to one thread and fixing health reporting, and #823 by installing mlx-lm/mlx-audio in setup.
Out of Scope Changes check ✅ Passed The additional MPS and cache-cleanup changes are consistent with the stated objectives and supporting follow-up work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/backends/mlx_backend.py`:
- Around line 97-101: Route the unload operation in the model-size change branch
through _run_on_mlx_thread instead of calling unload_model() directly. Await the
queued unload before the existing _load_model_sync call so unload_model remains
serialized with in-flight generate or load operations on the dedicated MLX
worker.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 65226774-a58d-43a9-b033-44aee1c1b6d5

📥 Commits

Reviewing files that changed from the base of the PR and between f2cf2a7 and c82185f.

📒 Files selected for processing (4)
  • backend/backends/mlx_backend.py
  • backend/requirements-mlx.txt
  • backend/routes/health.py
  • justfile

Comment thread backend/backends/mlx_backend.py
…e Silicon

get_torch_device() already supports allow_mps, and other engines
(luxtts_backend.py, qwen_llm_backend.py) already pass allow_mps=True —
but PyTorchTTSBackend, PyTorchSTTBackend, and QwenCustomVoiceBackend
never did, so any Apple Silicon install that ends up on the PyTorch
backend (e.g. MLX unavailable, or before this PR's justfile fix)
silently ran these three on plain CPU instead of falling back to
PyTorch+MPS.

This turned out to matter more than expected: benchmarking on M4
hardware, PyTorch+MPS generation (RTF ~1.7x) was faster than this
same PR's MLX thread-fix (RTF ~3.4-4.3x) for the same qwen engine and
model size, and faster than a CUDA RTX 2080 Ti (RTF ~2.3-3.1x) too.
I'm not proposing changing the default backend selection here — one
engine, one model size, one machine is not enough data for that, and
MLX-Whisper is reportedly faster than PyTorch-CPU for STT — but MPS
should clearly be an option rather than silently absent.

Verified on M4: PyTorchTTSBackend()._get_device() and
QwenCustomVoiceBackend()._get_device() both now return "mps" (were
"cpu").
@AlexStephenLytton

Copy link
Copy Markdown
Author

Added a second, related commit: enabling MPS for the PyTorch Qwen3-TTS/CustomVoice engines (`allow_mps=True` was already used elsewhere — `luxtts_backend.py`, `qwen_llm_backend.py` — just missing here).

While testing the MLX thread fix I benchmarked PyTorch+MPS directly against MLX for the same engine/model/machine (M4, `qwen` engine, 0.6B), out of curiosity about whether MLX's "4-5x faster than PyTorch" claim held up now that both paths actually work:

Backend RTF
MLX (this PR's thread fix) ~3.4-4.3x
PyTorch + MPS (this commit) ~1.7x
PyTorch + CUDA, RTX 2080 Ti, for reference ~2.3-3.1x

PyTorch+MPS came out faster than MLX on the same hardware, and faster than a discrete CUDA GPU too. Small sample (one text, one model size, one run each) so take the exact numbers with a grain of salt, but the gap is large enough that it seemed worth surfacing rather than just leaving PyTorch+MPS unreachable. Not proposing changing default backend selection — MLX-Whisper is reportedly faster than PyTorch-CPU for STT per the docs, so the right choice may differ per engine/task — just that MPS should be an available option instead of silently falling back to CPU whenever the PyTorch path is used.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/backends/qwen_custom_voice_backend.py`:
- Line 68: Update unload_model to release the MPS allocator cache in addition to
the existing CUDA cleanup, using the shared device-cache helper if available or
an MPS-specific branch. Ensure unloading on Apple Silicon clears unused MPS
memory when switching models.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4c8ba5e3-2a31-489b-91f1-bd23604c984e

📥 Commits

Reviewing files that changed from the base of the PR and between c82185f and 7cfa6b8.

📒 Files selected for processing (2)
  • backend/backends/pytorch_backend.py
  • backend/backends/qwen_custom_voice_backend.py

Comment thread backend/backends/qwen_custom_voice_backend.py
…ad thread-safety

Two follow-ups from the automated review on this PR:

1. backend/backends/base.py: empty_device_cache() only handled "cuda"
   and "xpu", never "mps" — so once the previous commit enabled MPS for
   the PyTorch Qwen3-TTS/CustomVoice engines, switching model sizes on
   Apple Silicon would leak MPS-cached memory (torch.mps.empty_cache()
   never got called). Added the missing branch.

2. backend/backends/qwen_custom_voice_backend.py: unload_model() had
   its own inline `if torch.cuda.is_available(): torch.cuda.empty_cache()`
   instead of using the shared empty_device_cache() helper — meant it
   never freed MPS memory either, same bug as jamiepine#1 but duplicated instead
   of shared. Switched to the shared helper (matches pytorch_backend.py's
   existing pattern).

3. backend/backends/mlx_backend.py: load_model_async()'s unload-on-
   model-size-change call went straight to self.unload_model() instead
   of routing through _run_on_mlx_thread like every other MLX call in
   this file (the whole point of the earlier commit in this PR). Fixed
   for consistency with the thread-pinning fix.

Verified on M4: 0.6B -> generate still works post-changes (regression
check), torch.mps.empty_cache() confirmed callable without error in
this environment.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/backends/base.py`:
- Around line 185-186: Update the MPS branch in the cache-clearing logic to
require torch.backends.mps.is_available() before calling
torch.mps.empty_cache(), while preserving the existing device == "mps" condition
and CUDA behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2308a9f0-630b-4dbc-b2b2-58a0def8fe01

📥 Commits

Reviewing files that changed from the base of the PR and between 7cfa6b8 and e0553aa.

📒 Files selected for processing (3)
  • backend/backends/base.py
  • backend/backends/mlx_backend.py
  • backend/backends/qwen_custom_voice_backend.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/backends/qwen_custom_voice_backend.py
  • backend/backends/mlx_backend.py

Comment thread backend/backends/base.py
Comment on lines +185 to +186
elif device == "mps" and hasattr(torch, "mps"):
torch.mps.empty_cache()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the changed area and related helpers in backend/backends/base.py
FILE="backend/backends/base.py"
wc -l "$FILE"
sed -n '140,220p' "$FILE"

# Find related uses of empty_device_cache and device detection helpers
rg -n "empty_device_cache|get_torch_device|mps|torch\.mps|torch\.backends\.mps" backend -S

Repository: jamiepine/voicebox

Length of output: 10703


🌐 Web query:

PyTorch mps empty_cache availability check torch.backends.mps.is_available docs

💡 Result:

In PyTorch, torch.mps.empty_cache is a standard function available as part of the MPS (Metal Performance Shaders) backend [1][2]. It is used to release unoccupied cached memory held by the caching allocator, which can help reclaim memory for other GPU applications [3][2]. Regarding your query about checking availability: 1. MPS Availability Check: To check if MPS is available in your current environment, you should use torch.backends.mps.is_available [4][5]. This function returns a boolean indicating whether the MPS backend is supported and currently available [4][6]. 2. Function Location: Note that while torch.backends.mps.is_available is used for checking availability [4][7], the empty_cache function resides under the torch.mps namespace (i.e., torch.mps.empty_cache) [1][3]. It is not located under torch.backends.mps. Recent updates to PyTorch have also enhanced torch.mps.empty_cache to clear the MPSGraphCache in addition to the standard allocator pool, which is particularly useful for workloads involving variable tensor shapes [8]. Summary of common MPS-related checks: - Check if MPS is available: torch.backends.mps.is_available [4][7] - Check if PyTorch was built with MPS support: torch.backends.mps.is_built [7][6] - Clear MPS cache: torch.mps.empty_cache [1][2]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import importlib.util, sys
spec = importlib.util.find_spec("torch")
print("torch_installed=", spec is not None)
if spec is not None:
    import torch
    print("torch_version=", getattr(torch, "__version__", "unknown"))
    print("hasattr(torch, 'mps')=", hasattr(torch, "mps"))
    print("hasattr(torch.backends, 'mps')=", hasattr(torch.backends, "mps"))
    if hasattr(torch.backends, "mps"):
        try:
            print("torch.backends.mps.is_available()=", torch.backends.mps.is_available())
        except Exception as e:
            print("torch.backends.mps.is_available() raised:", type(e).__name__, e)
    if hasattr(torch, "mps"):
        try:
            print("torch.mps exists; empty_cache attr=", hasattr(torch.mps, "empty_cache"))
        except Exception as e:
            print("torch.mps probe raised:", type(e).__name__, e)
PY

Repository: jamiepine/voicebox

Length of output: 179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect git context around the file without running repository code
git diff -- backend/backends/base.py | sed -n '1,220p'

Repository: jamiepine/voicebox

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on the specific helper implementation and surrounding device logic.
FILE="backend/backends/base.py"
nl -ba "$FILE" | sed -n '160,205p'

Repository: jamiepine/voicebox

Length of output: 196


🌐 Web query:

Does torch.mps.empty_cache raise when torch.backends.mps.is_available is false

💡 Result:

Yes, calling torch.mps.empty_cache when the MPS (Metal Performance Shaders) backend is not available raises a RuntimeError with the message "Cannot execute emptyCache without MPS backend" [1][2]. Because torch.mps.empty_cache interacts directly with the underlying MPS allocator [3][4], it is not safe to call on non-Apple Silicon devices or environments where the MPS backend is not initialized [1][2]. To avoid this error in cross-platform code, you should guard the call by checking the availability of the backend first [1][5][2]: import torch if torch.backends.mps.is_available: torch.mps.empty_cache

Citations:


Guard the MPS cache clear with a backend availability check.

hasattr(torch, "mps") only checks the namespace; torch.mps.empty_cache() can still raise when MPS isn’t available. Mirror the CUDA branch and gate this on torch.backends.mps.is_available().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/backends/base.py` around lines 185 - 186, Update the MPS branch in
the cache-clearing logic to require torch.backends.mps.is_available() before
calling torch.mps.empty_cache(), while preserving the existing device == "mps"
condition and CUDA behavior.

@shsunmoonlee

Copy link
Copy Markdown

Confirming this fix on more hardware, plus one gap found while testing it.

Confirmation — Apple M3 Pro, 18 GB, macOS 26.5 (Darwin 25.5.0), dev checkout (Python 3.12.8, mlx 0.32.0, mlx-lm 0.31.1, mlx-audio 0.4.1):

Gap — the same thread-affinity crash still exists in backend/backends/qwen_llm_backend.py: MLXQwenLLMBackend.load_model/generate still go through asyncio.to_thread(), so with this PR applied, POST /llm/generate (personality Compose/Rewrite, dictation refinement) fails 100% with the identical error, raised from mlx_lm.generate's wired_limit on exit.

Opened #909 (built on top of this branch) routing the LLM backend through the same _run_on_mlx_thread helper — verified on the hardware above. @AlexStephenLytton feel free to pick that commit into this PR instead if you'd rather keep the fix in one place.

🤖 Generated with Claude Code

@leighton-tidwell

Copy link
Copy Markdown

Affected on Voicebox 0.5.0 / macOS 26.5.1 / Apple Silicon. Same MLX thread-affinity crash (There is no Stream(gpu, N) in current thread). Note the LLM-refinement path still uses asyncio.to_thread in qwen_llm_backend.py even with this applied — #909 extends this fix to that backend, which is where our crash surfaces. Confirming the base fix here is needed; happy to test.

@jamiepine

Copy link
Copy Markdown
Owner

Thanks for this — two of the three things here were real. The thread-affinity crash is fixed on 0.6-dictation-fixes via backend/services/mlx_thread.py (single-worker executor, load+infer as one job), which supersedes the threading half. And the just setup-python gap — mlx-lm/mlx-audio not installing on Apple Silicon so fresh dev setups silently fall back to PyTorch — is a genuine catch we're tracking for 0.6.

The part we can't take is the MPS device-policy change bundled in with the fix; that alters generation behavior beyond the crash being fixed and would need its own PR with its own justification. Closing this one, but the setup-python find is appreciated.

@jamiepine jamiepine closed this Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants