Fish Audio Drama 3#
Fish Audio Drama 3 is Fish Audio’s new-generation text-to-speech model, built for multi-character scenes and long-form content. It supports multi-speaker generation, voice cloning and natural-language control.
35B parameters, 3B active per token: MoE text backbone with hybrid (full + linear) attention
Codebook-MTP audio decoder: 10 codebooks decoded causally over the whole sequence
FP8 MoE experts with the remaining weights in bf16; an int4 checkpoint cuts the weight memory from 37.0 GB to 20.8 GB
Speaker tags
<|speaker:0|>…<|speaker:15|>— one text can carry up to 16 charactersLong text is chunked automatically and can be streamed per chunk
Installation and Startup#
Detailed instructions are in Installation. From the repository root:
uv pip install --prerelease=allow -v -e ".[fishaudio]"
Download the checkpoint, then start the server. This command places preprocessing, the engine, and the vocoder on GPU 0. A stage moves to another GPU only when its --<stage>.gpu flag says so; see Deployment.
hf download fishaudio/drama-3
sgl-omni serve \
--model-path fishaudio/drama-3 \
--port 8000
Synthesize Speech#
The server supports plain text-to-speech, optionally cloning a voice from reference audio, returned as one response or as a stream.
Plain TTS:
curl -X POST http://localhost:8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "Hello, how are you?"
}' \
--output output.wav
Voice cloning, with a reference clip and its transcript:
curl -X POST http://localhost:8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "Get the trust fund to the bank early.",
"references": [{
"audio_path": "https://huggingface.co/datasets/zhaochenyang20/seed-tts-eval-mini/resolve/main/en/prompt-wavs/common_voice_en_10119832.wav",
"text": "We asked over twenty different people, and they all said it was his."
}]
}' \
--output output_voice_cloning.wav
Streaming:
curl -N -X POST http://localhost:8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"input": "Get the trust fund to the bank early.",
"stream": true,
"response_format": "pcm"
}' \
--output output_streaming.pcm
Example Usage#
The reference clip the samples below clone, unmodified:
The reference, en0 — a 3.9 s clip, the same common_voice_en_10119832.wav the cloning example above uses, with the transcript We asked over twenty different people, and they all said it was his.:
Editing as continuation#
Just send the previous VQ tokens and sent it as normal language models do.
{
"text": "The quarterly report is due on Thursday, so please finish it by noon.",
"references": [{"audio_path": "https://huggingface.co/datasets/zhaochenyang20/seed-tts-eval-mini/resolve/main/en/prompt-wavs/common_voice_en_10119832.wav", "text": "We asked over twenty different people, and they all said it was his."}],
"previous_takes": ["The quarterly report is due on Wednesday, so please finish it by noon."]
}
A second edit chained onto the result of the first:
K5 chains a second edit: by noon becomes by five, with both earlier takes in the context.
{
"text": "The quarterly report is due on Thursday, so please finish it by five.",
"references": [{"audio_path": "https://huggingface.co/datasets/zhaochenyang20/seed-tts-eval-mini/resolve/main/en/prompt-wavs/common_voice_en_10119832.wav", "text": "We asked over twenty different people, and they all said it was his."}],
"previous_takes": [
"The quarterly report is due on Wednesday, so please finish it by noon.",
"The quarterly report is due on Thursday, so please finish it by noon."
]
}
Serving Limits#
Reference Audio Length#
Each reference given as audio may be at most 60 seconds long. The limit applies to every reference in the request separately, and a longer one fails the request with HTTP 400 before it is encoded:
Drama 3 request: references.1: reference audio lasts 75.3 s, longer than the 60 s limit; shorten it or raise max_reference_audio_seconds
references.N names the reference by its position in the request’s references list, counted from 0.
Shorten that reference, or raise the limit when you start the server, for example to 90 seconds:
sgl-omni serve \
--model-path <checkpoint> \
--preprocessing.factory.max_reference_audio_seconds 90 \
--preprocessing.total_reserve_bytes 9GiB
or in a config file:
stages:
preprocessing:
factory:
max_reference_audio_seconds: 90
total_reserve_bytes: 9GiB
The GPU memory the preprocessing stage needs grows with the length of the reference it encodes, and the stage is capped at total_reserve_bytes (6 GiB by default, sized for the 60-second limit). When you raise the limit, raise the reserve with it, measured on your GPU; a reference that does not fit the reserve fails with an out-of-memory error in
the preprocessing stage. The budgets of all stages on the GPU must still fit the card, which the server checks at startup.
Prompt Length#
A prompt holds the tokens of the prompt template, the input text and the reference transcripts, plus one row for every codec frame of its reference audio, about 21.5 rows per second of audio (the codec turns 44.1 kHz audio into frames of 2048 samples). A prompt may have at most 8192 rows; a longer one fails the request with HTTP 400:
Drama 3 request: a prompt of 8193 rows exceeds the maximum allowed length of 8192 rows
Change the cap with --tts_engine.factory.max_prompt_rows. A prompt and its max_new_tokens must also fit the engine’s context length.
Attention Backend#
Drama 3 runs its attention on the triton backend only. The server refuses to start with any other attention backend, for example --tts_engine.engine.attention_backend fa3.
Int4 Checkpoint#
fishaudio/drama-3-int4 is a 4-bit GPTQ quantization of Drama 3. Thanks so much to AK for the quantization! The server reads its quantization from the checkpoint, so it starts with the same command:
hf download fishaudio/drama-3-int4
sgl-omni serve \
--model-path fishaudio/drama-3-int4 \
--port 8000
The routed MoE experts and the attention projections (q/k/v/o_proj of the full attention layers, in_proj_qkv, in_proj_z and out_proj of the linear attention layers) hold symmetric 4-bit weights with one scale per 128 input channels, and run on GPTQ-Marlin kernels. The shared expert, the router, the linear attention’s in_proj_a/b, the output head and the codebook-MTP audio decoder stay in bf16. The engine’s weights take 20.8 GB of GPU memory instead of 37.0 GB, which leaves more of the engine’s share for the KV cache. GPTQ-Marlin needs compute capability 8.0 or newer.
Table 3 compares the two checkpoints on one H200 with the default server settings: the 1,088 Seed-TTS English prompts, each cloning the voice of its own reference clip, sent as non-streaming requests at concurrency 8. WER is computed on the generated audio transcribed by Qwen3-ASR-1.7B.
Checkpoint |
Weights |
Latency mean |
Latency p95 |
RTF mean |
Throughput (req/s) |
WER (corpus) |
WER without samples above 50% |
|---|---|---|---|---|---|---|---|
|
37.0 GB |
1.235 s |
1.902 s |
0.281 |
6.47 |
1.99% |
1.32% |
|
20.8 GB |
1.334 s |
2.178 s |
0.320 |
5.99 |
1.63% |
1.42% |
Table 3. Drama 3 FP8 and int4 checkpoints on Seed-TTS English, concurrency 8, one H200.
The int4 checkpoint is about 8% slower per request and its WER stays at the FP8 level: the gap between the two is about as large as the run-to-run spread of one checkpoint (a second FP8 run measured 1.365 s mean latency and 2.02% WER).
Deployment Performance#
On one GPU the default placement works as is: every stage runs on GPU 0, with the vocoder in its own process. With two GPUs, put the vocoder on the second one, so its decoding no longer shares a GPU with the engine:
sgl-omni serve \
--model-path <checkpoint> \
--tts_engine.gpu 0 \
--vocoder.gpu 1
When the engine and the vocoder share one GPU, --mps on lets their processes share it through CUDA MPS (see Same-GPU Data Parallelism with CUDA MPS). It can raise throughput under high concurrency but may make a single stream slower; measure it with your own load before you keep it.
High Concurrency#
The defaults run at most 16 requests at a time on the engine and give it 0.70 of the GPU’s memory. For more streams at once, raise the request cap; the measurements below also raise the engine’s share:
sgl-omni serve \
--model-path <checkpoint> \
--tts_engine.gpu_memory_fraction 0.85 \
--tts_engine.factory.max_running_requests 256
These values are sized for an H200: the engine’s and the vocoder’s shares and the preprocessing reserve must fit one GPU together, which the server checks at startup.
Production Serving#
All numbers below were measured on NVIDIA H200 GPUs with streaming requests on Seed-TTS English prompts. Each request clones a voice from one pre-encoded reference (vq_codes with its transcript) and uses temperature 1.0, top_p 0.9, top_k 30 and max_new_tokens 500. Throughput counts generated acoustic frames per second. TTFA is the time to first audio. RTF is the time from sending a request to its last audio chunk, divided by the duration of the returned audio.
Concurrency |
Throughput (frames/s) |
TTFA p50 |
TTFA p95 |
RTF (mean) |
|---|---|---|---|---|
1 |
166.7 |
88.9 ms |
90.8 ms |
0.131 |
2 |
287.0 |
96.2 ms |
130.1 ms |
0.152 |
4 |
467.6 |
108.2 ms |
156.4 ms |
0.186 |
8 |
678.1 |
153.2 ms |
210.9 ms |
0.256 |
16 |
927.6 |
210.1 ms |
287.2 ms |
0.373 |
32 |
1,244.7 |
304.6 ms |
396.1 ms |
0.551 |
64 |
1,687.3 |
450.9 ms |
1,020.1 ms |
0.801 |
Table 1. Drama 3 streaming performance on one H200. Each cell is the mean of four runs.
A single stream gets its first audio in under 100 ms. At concurrency 16, audio is still generated about 2.7× faster than it plays back, and at concurrency 64 mean RTF stays at 0.80. In SGLang-Omni, how the pipeline stages are grouped into processes and placed on GPUs is deployment-time configuration rather than per-model code. Table 2 uses this to compare two vocoder placements at higher concurrency, with the vocoder’s default GPU share, 1,088 requests per concurrency level, and the average of six server starts. Its throughput also counts the end-of-speech frame that ends each stream.
Vocoder placement |
Concurrency |
Throughput (frames/s) |
TTFA p50 |
TTFA p95 |
|---|---|---|---|---|
Engine’s GPU |
128 |
2,177 |
685 ms |
1,412 ms |
Engine’s GPU |
256 |
2,422 |
3,403 ms |
4,255 ms |
Second GPU |
128 |
3,692 |
401 ms |
995 ms |
Second GPU |
256 |
4,160 |
2,146 ms |
2,469 ms |
Table 2. Drama 3 streaming performance by vocoder placement. Each cell is the mean of six server starts.
With the vocoder on the engine’s GPU, at 256 streams the vocoder is fully loaded and each new stream’s first audio waits in its queue. Moving it to a second GPU, as described under Deployment, gave about 72% more throughput and a 37% lower median TTFA at 256 streams. Use this layout when you need many concurrent streams and a short time to first audio together.
Non-Streaming Requests#
The vocoder decodes a non-streaming request’s audio in one piece after the engine finishes it, batching requests that
finish together up to a frame limit it sets at startup from its memory share (--vocoder.gpu_memory_fraction, 0.10
by default) and logs:
Drama 3 vocoder: a non-streaming decode takes up to <frames> padded frames of <bytes> bytes each; share <share> GiB, held <held> GiB
The limit does not count the memory the vocoder process holds outside PyTorch’s allocator, so it cannot guarantee that the process stays within its share. With the engine’s share raised to 0.85 on the same GPU, concurrent non-streaming requests can run out of GPU memory. To avoid that, stream those requests ("stream": true), keep the engine’s default share of 0.70 on the GPU that runs the vocoder, or put the vocoder on a second GPU, as under Deployment.
Defaults Parameters#
Default |
What it does |
Change it with |
|---|---|---|
Decode CUDA graph |
Replays decode steps as CUDA graphs. |
|
Breakable prefill CUDA graph, up to 2048 tokens |
Replays prefill batches of up to 2048 tokens, backbone and codebook tower together, as one breakable CUDA graph; larger batches run eagerly. Shortens the time to the first audio at the cost of startup time and GPU memory; the KV cache keeps its size. |
|
First-frame CUDA graphs, up to 16 requests |
Replays the step that samples each request’s first frame as a CUDA graph for prefills of up to 16 requests (at most the decode CUDA graph’s largest batch); larger prefills sample eagerly. Needs the decode CUDA graph; shortens each prefill at the cost of startup time and GPU memory. |
|
|
After a prefill, new prefills wait for the next 4 scheduling rounds, so running streams keep decoding. |
|
Sampling: |
Without a request |
The request’s |
Triton sampling |
Samples each frame’s main token and codes with Triton kernels instead of the PyTorch sampler, under the same top-k, top-p and repetition-aware rules. Using the same seed does not guarantee the same tokens as the PyTorch sampler. Rounding can keep or drop a candidate whose cumulative probability lies at top_p. |
|
Reference sampling, off |
Samples each frame with Fish Audio’s reference sampling for Drama 3, in PyTorch, instead of the samplers above. The main token’s first draw keeps only candidates whose probability reaches the request’s |
|
Fused codebook tower layers, up to 16 rows |
Runs the codebook tower layers of decode and first-frame steps of up to 16 rows as fused Triton kernels, eagerly or inside the CUDA graphs; larger steps and the tower’s prefill keep the module path. The kernels add in a different order, so the tower’s outputs can differ in the last bits and the sampled frames can differ. |
|
Vocoder window CUDA graphs |
Replays the decode of each streaming window length at batch 1 as a CUDA graph; batches and longer windows decode eagerly. |
|
Vocoder fp16 decoding |
Decodes frames into audio in fp16. |
|
The same settings in a config file:
stages:
tts_engine:
engine:
cuda_graph_backend_prefill: disabled
prefill_decode_interval: 0
vocoder:
factory:
window_cuda_graphs: false
decode_dtype: float32
Triton sampling and the fused codebook tower layers have been validated on Hopper GPUs only, so they run only on compute capability 9.0; on any other GPU no setting turns them on. Both also fall back without Triton or a CUDA device. Triton sampling falls back to the PyTorch sampler when its kernels do not cover the codebook size or the main-token candidate count, and the fused layers fall back to the module path with deterministic inference or when their kernels do not cover the tower weights or the KV cache. The startup log names the path each one takes and, for a fallback, the reason, for example:
Drama 3 codebook sampling: PyTorch, untested GPU sm_120
Drama 3 tower fused layers: off, untested GPU sm_120
Apple Silicon: native MLX#
The standalone fishaudio_drama3_server runs entirely in C++ on MLX under sglang_omni_mlx/native/ and serves /v1/audio/speech with the CUDA server’s request fields; reference audio is sent as base64 WAV (see the table below). It serves two community MLX conversions of fishaudio/drama-3, made by mlx-audio’s fish_drama3/convert.py from the official FP8 checkpoint:
Checkpoint |
Routed experts |
Other projections |
On disk |
Peak MLX memory, mlx-audio |
Unified memory |
|---|---|---|---|---|---|
|
4-bit |
8-bit |
24 GB |
28.7 GB |
48 GB or more |
|
8-bit |
bf16 |
42 GB |
47.1 GB |
64 GB or more |
Both quantize with MLX affine groups of 64 and keep the embeddings in bf16, the norm gains and the codec in fp32; the 8-bit checkpoint quantizes only the routed experts. The 4-bit checkpoint peaks above the GPU working set of a 32 or 36 GB Mac; the 8-bit one fits the 55.7 GB working set of a 64 GB Mac, with nothing else holding much memory. Both were measured on a 64 GB Mac. Download a pinned checkpoint, then build with the native runtime’s MLX 0.32.3 toolchain:
python sglang_omni_mlx/native/ci/provision_models.py "$DRAMA3_DATA" \
--model mlx-community/fish-audio-drama-3-4bit
sglang_omni_mlx/native/scripts/build_runtime.sh "$DRAMA3_RUNTIME" "$DRAMA3_BUILD"
"$DRAMA3_RUNTIME/bin/fishaudio_drama3_server" \
--model-path "$DRAMA3_DATA/models/mlx-community_fish-audio-drama-3-4bit" \
--model-name fishaudio/drama-3 --port 8000
Set DRAMA3_DATA, DRAMA3_RUNTIME and DRAMA3_BUILD to separate local directories; for the 8-bit checkpoint, replace 4bit with 8bit in both commands. Serving needs neither Python nor SGLang’s CUDA package. Voice cloning sends the reference clip, a mono 44.1 kHz WAV, as base64:
curl -X POST http://localhost:8000/v1/audio/speech \
-H "Content-Type: application/json" \
-d "{\"input\": \"Get the trust fund to the bank early.\",
\"references\": [{\"data\": \"$(base64 < reference_44k.wav | tr -d '\n')\",
\"text\": \"We asked over twenty different people, and they all said it was his.\"}]}" \
--output output_voice_cloning.wav
The server reads the request fields the CUDA server reads and refuses the same unsupported ones (seed included). Where they differ:
CUDA server |
Native MLX server |
|
|---|---|---|
Reference audio |
A path, URL or base64 data in any format the server decodes, resampled to 44.1 kHz; up to 60 s each by default |
Base64 mono 44.1 kHz PCM16 or float32 WAV, as |
References |
Any number, within the prompt row limit ( |
Up to 4, together at most 30 s (646 frames of |
Voices |
|
|
Long input |
One prompt; |
Split into chunks of about 300 bytes, each seeing the first and the previous chunk; |
Requests |
Batched |
One at a time; |
Streaming |
|
The same, with audio every 7 frames (0.33 s) |
fishaudio_drama3_synthesize runs one request from the command line and prints its timings; with --codes-output it also writes the first chunk’s prompt ids and every frame’s codes, for parity checks.
Reference parity and performance#
The numerical reference is mlx-audio 0.6.2 (f6d5da9: mlx_audio/tts/models/fish_drama3 and mlx_audio/codec/models/fish_s1_dac) on the same checkpoint and MLX 0.32.3; native/licenses/fishaudio_drama3-MIT.txt holds its license. The native runtime runs the gated delta rule on MLX’s mx::fast::gated_delta_update, where mlx-audio runs its own Metal kernel, and the two round their bf16 outputs differently. With mlx-audio’s gated delta step on the same MLX primitive and the same seed (given to fishaudio_drama3_synthesize), the native runtime samples the same codes and decodes the same PCM16 samples on every seeded test request.
Measured on an Apple M4 Max (64 GB), warm, in ABBA order (native, reference, reference, native). The server takes no seed, so each side ran each request three times; the table gives each round’s median real-time factor (generation time over audio duration) and the median time to the first streamed audio. Native times include the HTTP request and PCM encoding. The 4-bit checkpoint:
Request |
Real-time factor, native |
Real-time factor, mlx-audio 0.6.2 |
First streamed audio, native |
First streamed audio, mlx-audio |
|---|---|---|---|---|
English, no reference |
0.376 / 0.377 |
0.372 / 0.372 |
0.18 s |
0.18 s |
English, cloned voice |
0.375 / 0.373 |
0.375 / 0.372 |
0.18 s |
0.18 s |
Chinese, cloned voice |
0.367 / 0.370 |
0.366 / 0.366 |
0.18 s |
0.18 s |
Two speakers, cloned voice |
0.371 / 0.373 |
0.368 / 0.369 |
0.19 s |
0.18 s |
26 s passage, two chunks |
0.374 / 0.371 |
0.369 / 0.377 |
0.22 s |
0.22 s |
Loading takes 4.1 s native and 4.5 s for the reference, from a warm file cache. Peak MLX memory (mx.get_peak_memory()) is 28.2 GB native on the passage and 28.7 GB in the reference; the server’s process footprint stays near 32 GB under mixed load, against 23 GB idle.
The 8-bit checkpoint, measured the same way:
Request |
Real-time factor, native |
Real-time factor, mlx-audio 0.6.2 |
First streamed audio, native |
First streamed audio, mlx-audio |
|---|---|---|---|---|
English, no reference |
0.513 / 0.513 |
0.513 / 0.514 |
0.25 s |
0.25 s |
English, cloned voice |
0.507 / 0.506 |
0.510 / 0.506 |
0.25 s |
0.25 s |
Chinese, cloned voice |
0.500 / 0.499 |
0.502 / 0.501 |
0.25 s |
0.25 s |
Two speakers, cloned voice |
0.504 / 0.500 |
0.508 / 0.505 |
0.26 s |
0.26 s |
26 s passage, two chunks |
0.496 / 0.495 |
0.498 / 0.497 |
0.30 s |
0.30 s |
Loading takes 6.4 to 6.8 s native and 6.9 to 7.2 s for the reference. Peak MLX memory is 47.1 GB in the reference, and the server’s process footprint reaches 48 GB. Its routed experts are twice the size of the 4-bit ones and its other projections are bf16, and it runs about 1.35 times slower.