Skip to main content

omnivoice-fastpath

Inference speedups for OmniVoice TTS, with the measurements behind them. 4.6x faster than the stock Python API on the same hardware, at no quality difference we could detect over 48 paired sentences.

Half of this repo is negative results. Batching, ONNX fp32, CUDA graphs and CFG interval all sound like they should help. None of them do, and the numbers explaining why are more useful than another list of tricks.

Numbers

Apple M2 Max, MPS, fp16, 10 voice-cloning segments of 3-4s each.

Setup Total RTF Per segment
omnivoice-infer CLI, one process per segment 112.5s 3.36 11.25s
Python API, model resident, 32 steps, 6s reference 60.3s 1.81 6.03s
This repo — trimmed CFG + compile + 12 steps + 3s reference 13.1s 0.41 1.31s

The middle row is the honest baseline: 4.6x. The top row is 8.6x, but a third of that gap is just not reloading a 813M-parameter model for every sentence, which any serving setup fixes on its own.

Where the 4.6x comes from:

Lever Speedup Cost
32 -> 12 diffusion steps 2.34x none we could measure
6s -> 3s reference clip 1.37x none we could measure
Trimmed CFG 1.18x on MPS, 1.42x on CPU none, it is arithmetically lossless
torch.compile 1.09x on MPS, 1.42x on CUDA 10s startup on MPS, 137-218s on CUDA

Multiplied out that is 4.12x; measured is 4.61x, because torch.compile pays off more once the other levers have made each step cheaper.

Install

Not on PyPI yet — install from git:

pip install git+https://github.com/badrabbitt/omnivoice-fastpath

Or clone it, which you want anyway if you plan to run the benchmarks:

git clone https://github.com/badrabbitt/omnivoice-fastpath
cd omnivoice-fastpath && pip install -e ".[onnx,eval]"

omnivoice>=0.2.1 comes in as a dependency. Nothing here needs the OmniVoice source tree.

Use

import torch
from omnivoice.models.omnivoice import OmniVoice
from omnivoice_fastpath import apply_trimmed_cfg

model = OmniVoice.from_pretrained("k2-fsa/OmniVoice", device_map="mps", dtype=torch.float16)
apply_trimmed_cfg(model)                  # skip on CUDA, see below
model.llm = torch.compile(model.llm, dynamic=True)

prompt = model.create_voice_clone_prompt(ref_audio="ref_3s.wav", ref_text="...")
audio = model.generate(text="...", language="vi", voice_clone_prompt=prompt, num_step=12)

Reuse prompt across segments. Encoding a reference costs 1-2s, and a dubbing job is usually hundreds of segments sharing one voice.

Trimmed CFG

The one thing here that is not just parameter choice.

Every diffusion step runs a single forward over 2B rows: B conditional rows of length c_len (reference + text + target) and B unconditional rows of length u_len (target only). Both halves sit in one tensor, so the shorter unconditional rows get padded up to max_c_len. In voice-cloning mode we measured c_len=295 against u_len=89 — the unconditional half was doing 3.3x more work than it needed to, on every step.

Those padded positions attend only to themselves, and no real position attends to them, so dropping them cannot change the result. Verified at the logits level rather than argued: on CPU/fp32 the maximum absolute difference against the unpatched model is exactly 0.00000 with 100% argmax agreement. Under fp16 the difference is 0.22 with 98.4% agreement, which is GEMM reduction order changing with the tensor shape, not a different computation.

from omnivoice_fastpath import apply_trimmed_cfg
apply_trimmed_cfg(model)

It wraps forward, so upstream source stays untouched.

Turn it off on CUDA. Fewer tokens but twice the kernel launches: that trade wins where compute is the bottleneck and loses where launch latency is.

Device With trimmed CFG
CPU (fp32) 1.42x faster
MPS (fp16) 1.18x faster
CUDA T4 (fp16) 0.64x — slower

Steps

12 is the floor. 8 breaks.

Paired evaluation, 48 sentences, same seed per sentence across configs, judged by faster-whisper large-v3, 95% CI from a paired bootstrap over per-sentence WER.

Steps RTF WER Δ vs 32 steps 95% CI p
32 1.01 3.38% — — —
16 0.53 3.14% −0.24 [−1.63, +1.24] 0.695
12 0.43 2.51% −0.87 [−2.20, +0.52] 0.331
8 0.32 6.45% +3.07 [+1.04, +5.40] 0.009

12 steps is indistinguishable from 32 and 2.34x faster. Do not read 2.51% as better than 32 steps — the interval spans zero, that is noise. At 8 steps WER nearly doubles and sentences transcribed perfectly drop from 32/48 to 24/48.

Reference length

A reference clip is prepended to the sequence on every step, so its length is multiplied by the step count. Natural clips, each with its exact transcript:

Reference Tokens Mean seq len Total Speedup
3.1s 74 227.6 31.3s 1.37x
4.0s 96 249.1 36.4s 1.18x
5.2s 127 285.2 40.9s 1.05x
6.2s 149 314.0 42.9s —

Cutting 6.2s to 3.2s of the same speaker, with the transcript cut at the matching word boundary, showed no WER difference across 48 sentences (Δ −0.02, CI [−0.95, +0.92], p=0.754). WER does not measure timbre similarity, so listen before committing to a short reference for a voice you care about.

One trap: if you trim the audio and guess where to cut the transcript, the model's pacing estimate goes wrong and generates longer audio, which inflates RTF and makes the speedup look better than it is. Our first pass reported 1.86x for this reason. Cut on a word boundary and use the exact transcript.

What did not work

Idea Result
Batching segments Slower everywhere. T4 RTF 0.446 at batch 1 -> 0.584 at batch 32. Sorting by length first does not help.
ONNX Runtime fp32 Bit-identical to PyTorch, 2.7% faster per forward, net slower end to end once tensors round-trip through numpy.
ONNX Runtime int8 Genuinely 1.54x on macOS CPU, 2.3x on Linux CPU — but costs about 1 WER point, and CPU is 5x slower than the same machine's GPU anyway.
CUDA graphs (mode="reduce-overhead") 8% slower than default compile and 2.8x the compile time. Sequence length varies per sentence, so inductor re-records the graph per shape.
CFG interval 1.21x, but +2.18 WER points (CI [+0.74, +3.67], p=0.009). Dropping the early half is just as bad as the late half; guidance matters throughout.
CoreML execution provider 1.10x. Falls back to CPU for most of this model's ops.
FlashInfer on T4 KeyError: 'sm_75'. Its kernels need Ampere or newer.

Batching deserves a note, since it is the first thing everyone tries. Two reasons it fails: CFG already doubles the batch, so a single item saturates the device; and _generate_iterative scores tokens in a per-item Python loop each step, so batch 32 over 32 steps means 1024 small serialized operations. Making batching pay would mean vectorizing that loop upstream.

Devices

32 short voice-cloning segments, 32 steps, batch 1.

Device RTF Per segment Memory
Colab T4, fp16 0.45 1.79s 2.3GB VRAM
M2 Max MPS, fp16 1.37 5.52s 4.4GB
Docker CPU, fp32 9.85 39.97s 4.7GB

Voice cloning is much more expensive than auto-voice mode and the bill lands almost entirely on CPU: the same sentences run at RTF 4.39 without a reference and 9.85 with one, because the reference lengthens the sequence and attention is quadratic. Any RTF you see quoted for auto-voice is optimistic for real dubbing.

Long-form is cheaper per second than short segments on every device (T4: 0.33 vs 0.45) because each internal chunk is ~8s, so fixed per-call cost amortizes. Memory stays flat with length, so hours-long narration will not OOM. Hand the whole text to one generate() call and let OmniVoice chunk it — chunking it yourself and batching the pieces is slower.

Reproducing

pip install -e ".[onnx,eval]"

# headline comparison
python benchmarks/bench_vs_upstream.py --device mps --n 10 \
  --long-ref refs/ref_full.wav --long-ref-text "..." \
  --short-ref refs/ref_3s.wav --short-ref-text "..."

# cumulative effect of each lever
python benchmarks/bench_stack.py --device mps --long-ref ... --short-ref ...

# prove trimmed CFG changes nothing, at the logits level
python benchmarks/check_fastpath_parity.py --device cpu --dtype fp32 --ref ... --ref-text "..."

Quality evaluation is three steps: generate a set per config, transcribe with faster-whisper large-v3, then compare paired.

python eval/eval_stat_gen.py torch A_step32 32
python eval/eval_stat_asr.py A_step32 B_step12
python eval/eval_stat_report.py A_step32

The corpus is 48 Vietnamese sentences from a maths-explainer domain in benchmarks/eval_sentences.py. Swap it for your own language and content; the speed results are language-independent but the WER numbers are not.

Method notes

Everything above is measured on one machine (M2 Max, 12 CPU, 64GB; Docker limited to 12 CPU / 12GB) and one Colab T4. Numbers will move on other hardware, and the CPU/GPU trade-offs behind trimmed CFG and batching could invert on a different device.

Two things worth knowing if you extend this work.

The ASR judge matters more than the model under test. An earlier version of this evaluation used faster-whisper base and reported 20-33% WER with a completely different ranking of the configurations. large-v3 puts the same audio at 2-3%. If your judge is worse than your system, you are measuring the judge.

At these WER levels most sentences are ties, so the interesting statistic is how many sentences differ at all — 12 steps differs from 32 on 12 of 48 sentences, and only those carry any signal. The minimum difference this design can detect is about 1.3 WER points, so treat anything smaller as unresolved rather than equal.

Still on the table

The prefix — reference audio plus text — is 61-64% of the sequence and is completely static across every step, yet gets recomputed each time. Caching its KV states is what dLLM-Cache, Fast-dLLM and Elastic-Cache do for diffusion LLMs, all training-free. Counting tokens puts the ceiling here around 1.8x. It is an approximation — the prefix attends to the target bidirectionally, so freezing its KV changes the result — and nobody has published this for diffusion TTS specifically.

Guidance distillation removes the unconditional forward entirely for up to 2x, but needs fine-tuning.

Credits

OmniVoice is by the k2-fsa team, Apache-2.0. This repo adds nothing to the model — it wraps the published API and measures it. Benchmarks ran against 0.2.1 (commit 38e992b).

Apache-2.0.

Metadata

Release files for omnivoice-fastpath 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for omnivoice-fastpath 0.1.0
File Size Uploaded
omnivoice_fastpath-0.1.0.tar.gz 31.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for omnivoice-fastpath 0.1.0
File Interpreter ABI Platform
omnivoice_fastpath-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 44.9 kB

Release files / omnivoice_fastpath-0.1.0.tar.gz

Download URL omnivoice_fastpath-0.1.0.tar.gz
Size 31.0 kB
Tags Source
SHA-256 checksum
How to use checksums
453b25a55ca72abcfd4b1456da2be9418783587c52898894c22a292210951135
BLAKE2b-256 checksum
How to use checksums
c7f51f9856ad5f3d70eca4e61c5247fccb8e524e31e0006081c7cdca308bf711
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 17, 2026.

Transparency log

Release files / omnivoice_fastpath-0.1.0-py3-none-any.whl

Download URL omnivoice_fastpath-0.1.0-py3-none-any.whl
Size 13.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
379f3d783e9305243d35e351edcf070645b02c8f1f06fc02df080e609df60bbf
BLAKE2b-256 checksum
How to use checksums
4b6c872dd20d2947a52014a8f528a1f321fc89f868341f4e1a2f0e283132f8a9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 17, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page