Views
No views yet
mlx-audio.mlx-community/Zyphra-ZONOS2 (BF16)mlx-audio with the ZONOS2 model + batching. Install our optimized fork:pip install git+https://github.com/Amal-David/mlx-audio.git@zonos2-optimizedOPTIMIZATIONS.md.📁 Runnable scripts are inexamples/:generate.py(single / clone / long-form),batch_generate.py(throughput runner), andquantize.py(reproduce this build from BF16).
1from mlx_audio.tts import load
2from mlx_audio.audio_io import write as audio_write
3
4model = load("amal-david/Zyphra-ZONOS2-4bit", lazy=False)
5
6result = next(model.generate(
7 text="Hello, this is the four bit ZONOS two model running locally with MLX audio.",
8 max_tokens=1024,
9))
10audio_write("zonos2.wav", result.audio, result.sample_rate)
11print(result.audio_duration, "RTF", result.real_time_factor)1texts = [f"This is sample number {i}." for i in range(64)]
2
3for r in model.batch_generate(texts, max_tokens=1024, seed=42):
4 audio_write(f"out_{r.sequence_idx:03d}.wav", r.audio, r.sample_rate)Tip: group prompts of similar length in a batch. The loop runs until the longest sequence finishes, so mixing very short and very long prompts wastes compute.
1spk = model.extract_speaker_embedding("speaker.wav") # 2048-D, compute once
2result = next(model.generate(
3 text="This sentence is spoken in the cloned reference voice.",
4 speaker_embedding=spk, max_tokens=1024,
5))
6audio_write("cloned.wav", result.audio, result.sample_rate)max_tokens
(default 1024 ≈ 12 s) and quality is best within the model's window. For paragraphs/articles,
split on sentence boundaries, batch the chunks, and concatenate:1import re, mlx.core as mx
2
3def speak_long(model, text, ref_audio=None, max_chars=350, gap_s=0.12, seed=42):
4 sents = re.split(r"(?<=[.!?])\s+", text.strip())
5 chunks, cur = [], ""
6 for s in sents:
7 if len(cur) + len(s) > max_chars and cur:
8 chunks.append(cur); cur = s
9 else:
10 cur = f"{cur} {s}".strip()
11 if cur: chunks.append(cur)
12
13 spk = model.extract_speaker_embedding(ref_audio) if ref_audio else None # voice consistency
14 results = sorted(model.batch_generate(chunks, speaker_embedding=spk, max_tokens=1024, seed=seed),
15 key=lambda r: r.sequence_idx)
16 gap = mx.zeros((int(gap_s * model.sample_rate),))
17 pieces = []
18 for i, r in enumerate(results):
19 if i: pieces.append(gap)
20 pieces.append(r.audio)
21 return mx.concatenate(pieces, axis=0), model.sample_rate
22
23audio, sr = speak_long(model, open("article.txt").read())
24audio_write("long.wav", audio, sr)1python -m mlx_audio.tts.generate \
2 --model amal-david/Zyphra-ZONOS2-4bit \
3 --text "Hello from the quantized ZONOS two model." \
4 --output_path outputs --file_prefix zonos2_4bit| Component | Precision | Why |
|---|---|---|
MoE experts (SwitchGLU gate/up/down) — 94.5% of weights | 4-bit gs64 | The bandwidth lever; tolerates 4-bit |
Attention wq / wo | 8-bit gs64 | 4-bit attention → spurious early EOS; must stay ≥8-bit |
| Token embeddings | 8-bit gs64 | Phonetic stability |
Output head, MoE router, gater, per-head temperature, RMSNorm, ChunkedLinear (wkv/w_in) | bf16 | Quality-critical and/or tiny |
mlx-audio's converter:1from mlx_audio.tts.utils import convert
2
3SKIP = ("router", "multi_output", "gater", "norm", "temp")
4def predicate(path, module):
5 if any(s in path for s in SKIP): return False
6 if "experts" in path: return {"group_size": 64, "bits": 4}
7 if "attention" in path: return {"group_size": 64, "bits": 8} # 4-bit breaks EOS
8 if "embedders" in path: return {"group_size": 64, "bits": 8}
9 return False # keep bf16
10
11convert("mlx-community/Zyphra-ZONOS2", "Zyphra-ZONOS2-4bit",
12 quantize=True, q_group_size=64, q_bits=4, q_mode="affine",
13 quant_predicate=predicate)| Metric | BF16 base | This 4-bit |
|---|---|---|
| Weights on disk | 15.34 GB | 4.68 GB |
| Single-stream RTF (lower = faster) | ~1.40 | ~0.85 |
| Forward-only compute | 1.0× | ~1.6× |
| Batched throughput (B=32–96) | — | ~4.0–4.35× real-time, ~350–375 frames/s |
| Peak memory @ B=32 | ~19 GB | ~12 GB |
mx.async_eval + mx.compile)
attacks the second. They stack to roughly 2.5–3× single-stream.mx.set_wired_limit(...) to avoid paging on long runs; run a 1–2 token
warmup before timing (first run pays ~2× Metal kernel compilation); export MLX_METAL_FAST_SYNCH=1;
keep decode single-process (concurrent processes contend for the one GPU + bandwidth)..tolist() per sequence) —
~+17% batch throughput at B=32, byte-identical (verified). Implemented in the fork.mx.async_eval + mx.compile): forward-bound at RTF ~0.82; measured
mx.compile gain ~1.0–1.1× — not worth it. ChunkedLinear quant gave no forward gain either
(tested). The single-stream forward is at its bandwidth/dispatch floor.⚠️ Quality: 4-bit experts are perceptually close to BF16 on typical utterances but can show intermittent artifacts on hard/outlier cases (top-1 MoE). A blind A/B vs the BF16 base is recommended before production use; an 8-bit build is the safe-fidelity fallback.
Amal-David/mlx-audio @ zonos2-optimized — quantization recipe, batching tooling, and benchmarks (this model is built with it)mlx-audio (Prince Canuma, MIT)