Dictation Metal-probe panics on macOS 14 ("class MTLResidencySetDescriptor could not be found")
- Langage dominant
- Rust
- Étoiles
- 54.2k
- Forks
- 6.2k
- Merge moyen
- 3 j 2 h
- PR mergées (30 j)
- 262
Description
**Describe the bug**
On macOS 14.x, any code path that probes for a Metal device for local dictation
(`candle_core::Device::new_metal(0)`, used by `crates/goose/src/dictation/whisper.rs` to pick a
Whisper model size and to select the transcription device) panics instead of returning an `Err`.
The panic happens in `objc2-metal` (0.3.2, pulled in transitively by `candle-core` 0.11.0), which
does an unconditional Objective-C class lookup for `MTLResidencySetDescriptor` — a class that does
not exist in the Metal framework shipped with macOS 14; it was introduced in macOS 15. Because the
lookup panics rather than returning `None`/`Err`, `Device::new_metal(0)` never gets the chance to
report "Metal unavailable" the way its API contract implies — the whole call unwinds instead.
In the desktop app this fires inside a `tokio-rt-worker` thread while handling a dictation-config
request. The panic takes down that in-flight async task without crashing the whole process, which
leaves the frontend hung waiting on a response that will never arrive — observed side effects
included Session History stuck on loading skeletons and the model-switcher no longer responding
until the app was restarted.
This is a different failure mode from #7674 (closed): that one was a *build-time* dyld symbol
resolution error when linking against the macOS 14 SDK, fixed by pinning the build SDK. This one is
a *runtime* Objective-C class lookup on a class that simply doesn't exist pre-macOS 15 — the binary
links and starts fine, so the SDK-pin workaround has no effect on it.
---
**To Reproduce**
Steps to reproduce the behavior:
1. Run the desktop app on macOS 14.x (tested on 14.8.3).
2. Open Settings — this lands on the "Chat" tab, which mounts the Dictation settings component and
fetches the dictation config for all providers, including `Local`.
3. Fetching the `Local` provider's default model calls `whisper::recommend_model()`, which calls
`Device::new_metal(0)` to decide whether a GPU is available.
4. The backend logs a panic and the request that triggered it never completes.
---
**Expected behavior**
`Device::new_metal(0)` (or the caller) should treat "Metal API unavailable on this OS version" as a
graceful `Err`/`None`, matching its documented contract, rather than panicking and abandoning the
in-flight async task.
---
**Screenshots**
N/A (backend-only panic, visible in log output):
```
thread 'tokio-rt-worker' panicked at objc2-metal-0.3.2/src/generated/MTLResidencySet.rs:10:1:
class MTLResidencySetDescriptor could not be found
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```
---
**Suggested fix**
The most durable fix is upstream in `objc2-metal`/`candle-core` (don't probe a class that doesn't
exist pre-macOS 15, or return `Err` instead of panicking). Short of that, `goose` itself can defend
both `Device::new_metal(0)` call sites in `crates/goose/src/dictation/whisper.rs` with
`std::panic::catch_unwind`, treating a caught panic the same as the `Err` the call should have
produced:
```rust
/// `Device::new_metal` is documented to return `Err` when Metal is unavailable, but on some
/// OS/driver combinations (observed: macOS 14, where a Metal API introduced in macOS 15 is
/// looked up unconditionally) the underlying objc2 binding panics instead of erroring. Every
/// caller here only wants a best-effort "is Metal usable" signal, so treat a panic the same as
/// the `Err` it should have been rather than taking the whole request down with it.
fn try_new_metal_device() -> Option {
match std::panic::catch_unwind(|| Device::new_metal(0)) {
Ok(Ok(device)) => Some(device),
Ok(Err(_)) | Err(_) => None,
}
}
```
`recommend_model()`'s capability check becomes
`Device::new_cuda(0).is_ok() || try_new_metal_device().is_some()`, and the device-selection branch
in `WhisperTranscriber::new_with_tokenizer()` becomes
`} else if let Some(device) = try_new_metal_device() { ... }`, both replacing a bare
`Device::new_metal(0)` call. This is a minimal, local mitigation — it doesn't fix the underlying
class-lookup gap, just stops it from taking down an in-flight request. Happy to open a PR with this
change if useful.
---
**Please provide the following information**
- **OS & Arch:** macOS 14.8.3 (Sonoma), arm64
- **Interface:** UI (desktop)
- **Version:** 1.48.0 (also present at least back through recent releases carrying `candle-core` 0.11.0 / `objc2-metal` 0.3.2)
- **Extensions enabled:** N/A — reproduces with no extensions, purely from opening Settings
- **Provider & Model:** N/A — this is the local dictation/Whisper capability probe, independent of the chat provider/model in use
---
**Additional context**
Root cause is upstream in `objc2-metal` 0.3.2 (via `candle-core` 0.11.0): the
`MTLResidencySetDescriptor` lookup is unconditional rather than falling back when the class is
absent. A version bump once upstream fixes the lookup, or an OS-version guard before probing Metal
at all, would be the more durable long-term fix; see "Suggested fix" above for the local mitigation
applied downstream in the meantime.
Guide de contribution
Ouvrir le guide de contribution
Évaluation
Cette issue n'a pas encore été évaluée.