Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Documentation/Diarization/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ let diarizer = DiarizerManager(config: config)

> Requires macOS 14 / iOS 17 or later. The offline stack uses native C++ clustering and AsyncStream coordination that are unavailable on older OS releases.

> **Known OS issue — macOS 14 can crash in BNNS.** macOS 14 (all patch levels through at least 14.8.7) carries an Apple bug that crashes Core ML predictions on the BNNS CPU path (`EXC_BAD_ACCESS` in `libBNNS`, `BNNSGraphContextExecute_v2` → `_platform_memmove`). Offline diarization is the component most exposed because its FBank model always runs on CPU. Nothing at the library or app level avoids it — serialized pipelines, single-model runs, and every compute-unit routing all crash ([#878](https://github.com/FluidInference/FluidAudio/issues/878), 1200/1200 reproduction on macOS 14 CI runners; also [#661](https://github.com/FluidInference/FluidAudio/issues/661)). On machines without a Neural Engine (VMs, CI runners) the crash is deterministic; on Apple Silicon it is intermittent, striking when predictions fall back from the ANE to BNNS. Apple fixed it in macOS 15 — the only remedy is updating the OS. `OfflineDiarizerManager` logs a warning when initialized on an affected build. (This is a distinct bug from the macOS/iOS 26.4–26.5 BNNS crash documented for Kokoro TTS in [KokoroAne.md](../TTS/KokoroAne.md).)

When you need full parity with the pyannote/Core ML exporter (powerset segmentation + VBx clustering), use `OfflineDiarizerManager`. It orchestrates segmentation, soft mask interpolation, WeSpeaker embedding extraction, PLDA/VBx clustering, and timeline reconstruction in one place:

```swift
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,35 @@ public final class OfflineDiarizerManager {
}

public func initialize(models: OfflineDiarizerModels) {
if Self.isBnnsCrashProneOS(ProcessInfo.processInfo.operatingSystemVersion) {
logger.warning(
"macOS 14 has a known Apple BNNS bug that can crash offline "
+ "diarization (EXC_BAD_ACCESS in libBNNS) regardless of compute-unit "
+ "routing or serialization. Fixed in macOS 15. "
+ "See https://github.com/FluidInference/FluidAudio/issues/878")
}
self.models = models
logger.info("Offline diarizer models initialized")
}

#if os(macOS)
private static let runningOnMacOS = true
#else
private static let runningOnMacOS = false
#endif

/// macOS 14 carries an Apple BNNS bug that crashes Core ML predictions on
/// the BNNS CPU path (`BNNSGraphContextExecute_v2` → `_platform_memmove`,
/// #661/#878). Deterministic on machines without an ANE, intermittent on
/// Apple Silicon when predictions fall back from the ANE. No usage pattern
/// avoids it; Apple fixed it in macOS 15. iOS is unflagged — no reproduction
/// has been reported on the iOS 17 line.
static func isBnnsCrashProneOS(
_ version: OperatingSystemVersion, onMacOS: Bool = runningOnMacOS
) -> Bool {
onMacOS && version.majorVersion == 14
}

/// Ensure offline diarizer models are available, downloading and compiling them when needed.
/// - Parameters:
/// - directory: Custom cache directory. Defaults to `OfflineDiarizerModels.defaultModelsDirectory()`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ public struct OfflineDiarizerModels: Sendable {
throw OfflineDiarizationError.modelNotLoaded(ModelNames.OfflineDiarizer.pldaRho)
}

// CPU is fastest for FBank, but on macOS 14 this pins every FBank
// prediction to the crash-prone BNNS path (#878).
let fbankComputeUnits: MLComputeUnits = .cpuOnly
let fbankModels = try await ModelHub.loadModels(
.diarizer,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import XCTest

@testable import FluidAudio

final class OfflineDiarizerOsAdvisoryTests: XCTestCase {

private func version(_ major: Int, _ minor: Int, _ patch: Int = 0) -> OperatingSystemVersion {
OperatingSystemVersion(majorVersion: major, minorVersion: minor, patchVersion: patch)
}

// #878: the entire macOS 14 line crashes in libBNNS regardless of
// patch level (1200/1200 on 14.8.7 CI runners).
func testMacOS14LineIsFlagged() {
XCTAssertTrue(OfflineDiarizerManager.isBnnsCrashProneOS(version(14, 0), onMacOS: true))
XCTAssertTrue(OfflineDiarizerManager.isBnnsCrashProneOS(version(14, 6, 1), onMacOS: true))
XCTAssertTrue(OfflineDiarizerManager.isBnnsCrashProneOS(version(14, 8, 7), onMacOS: true))
}

func testFixedAndUnaffectedMacOSLinesAreNotFlagged() {
XCTAssertFalse(OfflineDiarizerManager.isBnnsCrashProneOS(version(13, 6), onMacOS: true))
XCTAssertFalse(OfflineDiarizerManager.isBnnsCrashProneOS(version(15, 0), onMacOS: true))
XCTAssertFalse(OfflineDiarizerManager.isBnnsCrashProneOS(version(15, 7, 7), onMacOS: true))
XCTAssertFalse(OfflineDiarizerManager.isBnnsCrashProneOS(version(26, 5, 2), onMacOS: true))
}

// No reproduction reported on iOS; only macOS is flagged.
func testIOSIsNotFlagged() {
XCTAssertFalse(OfflineDiarizerManager.isBnnsCrashProneOS(version(14, 0), onMacOS: false))
XCTAssertFalse(OfflineDiarizerManager.isBnnsCrashProneOS(version(17, 0), onMacOS: false))
XCTAssertFalse(OfflineDiarizerManager.isBnnsCrashProneOS(version(18, 0), onMacOS: false))
}
}
Loading