Skip to content

MLX talker backend + SpeechDecoder overflow chunking - #3

Open
dbrkn wants to merge 3 commits into
berkin/voice-clone-coremlfrom
berkin/voice-clone-coreml-talker
Open

MLX talker backend + SpeechDecoder overflow chunking#3
dbrkn wants to merge 3 commits into
berkin/voice-clone-coremlfrom
berkin/voice-clone-coreml-talker

Conversation

@dbrkn

@dbrkn dbrkn commented Jul 20, 2026

Copy link
Copy Markdown
Owner

This adds two things on top of the voice-clone base:

  1. Extensions/TTSKitMLX — an opt-in MLX-Swift port of the Qwen3-TTS talker (CodeDecoder). Voice-clone ICL prefixes are 150–300+ tokens, and the CoreML talker prefills one position per call (~50 tok/s ⇒ seconds of time-to-first-audio per clone) with a compile-time KV cap on prompt + generation. The MLX talker prefills the whole prefix in one batched forward (~900 tok/s measured on M-series, ~17×) and has no KV cap; decode-step latency is at parity (steps are dominated by the CoreML MultiCodeDecoder/SpeechDecoder). Parity vs the Python reference: prefill logits cosine 1.000000, 20/20 greedy-decode tokens exact. Lives in a nested package because mlx-swift floors at macOS 14 (no watchOS) and SwiftPM platform floors are package-wide — TTSKit itself gains no dependency and keeps its platforms. Injected via the existing TTSKitConfig.codeDecoder override; ttskit-mlx-cli tts drives the full hybrid (CoreML encoders + MLX talker + CoreML decode) end-to-end.

  2. SpeechStreamWriter: KV-cache re-prime on overflow. Generations longer than the SpeechDecoder's cache window (~21s on the kv_len_256 asset) previously clamped KV writes with only a log line, silently degrading the audio tail. When the cache fills, the writer now resets it and re-decodes the last 24 frames as context (audio discarded — already emitted), then continues. Unit-tested: 300 frames through a 256-slot cache, byte-exact emitted audio.

Requires: the Base-family mlx-community talker checkpoint (opt-in path only; a pruned talker-only export is a follow-up).

@dbrkn
dbrkn force-pushed the berkin/voice-clone-coreml-talker branch 2 times, most recently from ef6208f to a95a911 Compare July 21, 2026 12:56
@dbrkn
dbrkn force-pushed the berkin/voice-clone-coreml branch from f75887d to 99611a3 Compare July 21, 2026 12:56
@dbrkn
dbrkn force-pushed the berkin/voice-clone-coreml-talker branch from a95a911 to 730e3ef Compare July 21, 2026 13:40
dbrkn and others added 2 commits July 21, 2026 19:23
Generations longer than the SpeechDecoder cache window (~21s for
kv_len_256) previously clamped KV writes with only a log line, silently
degrading the audio tail. Port the Python reference's windowed-decode
semantics to the streaming design: when the cache fills, reset it and
re-decode the last 24 frames as context (audio discarded — already
emitted), then continue. Extra cost only on long generations (~10% of
decode calls per window). Unit-tested with a cache-advancing mock: 300
frames through a 256-slot cache, byte-exact emitted audio, >=1 re-prime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add an MLX-Swift port of the Qwen3-TTS talker as an opt-in CodeDecoder
backend for voice cloning, integrated into the existing argmax-cli (no
second CLI). `argmax-cli tts --code-decoder-backend mlx` swaps the
CoreML talker for `MlxCodeDecoder` via the `TTSKitConfig.codeDecoder`
override; every other component (voice-clone reference encoders,
embedders, MultiCodeDecoder, SpeechDecoder) stays on CoreML. The MLX
talker prefills the whole ICL prefix in one batched forward (~17x
faster time-to-first-audio on 150-300-token voice-clone prompts) and
has no compile-time KV cap. Default remains coreml; --mlx-model-dir
and --mlx-max-sequence-length configure the MLX path.

The new TTSKitMLX library target lives in the root package: mlx-swift
floors at macOS 14, so the root macOS platform floor is bumped 13 -> 14.
This is justified because TTS generation already requires macOS 15 at
runtime (multifunction SpeechDecoder guard); iOS 16 / watchOS 10 /
visionOS 1 floors are unchanged. The MLX products are macOS-conditional
dependencies and all TTSKitMLX sources compile behind #if canImport(MLX),
so iOS/watchOS/visionOS builds compile the target as empty.

A `graft-mlx-metallib` Makefile target encapsulates the known
workaround for running the mlx backend from a command-line SwiftPM
build (xcodebuild compiles mlx-swift's Metal shaders into
mlx-swift_Cmlx.bundle; graft it next to the release binary).

TalkerParityTests assert parity against goldens exported from the
Python mlx-audio reference: batched-prefill last-position logits and
hidden cosine 1.000000, 20/20 greedy decode tokens exact, batched ==
sequential prefill, and KV bookkeeping through the TTSKit KVCache
interface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dbrkn
dbrkn force-pushed the berkin/voice-clone-coreml-talker branch from 730e3ef to 3c30e2d Compare July 21, 2026 16:29
The MLX talker keeps a single private KV cache per decoder instance;
concurrent chunk workers interleave batched prefills into it, crashing
with broadcast_shapes mask/score mismatches or MLXArray retain-count
faults on multi-chunk texts. Force --concurrent-worker-count 1 for
--code-decoder-backend mlx (matches the sequential Python prototype).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant