A small, standalone gpt-fast-style static-shape inference path for the public
Audio8-TTS-Preview-0.6b
text-to-speech model (the arktts architecture). It reuses the model's public
weights through its own static-shape forward code and compiles the whole
per-audio-frame step into a single CUDA graph — taking one batch-1 stream from
RTF ~0.54 (eager reference) down to ~0.091 (~11x real time) on a laptop-class
RTX 5090, which is close to the card's memory-bandwidth floor.
Nothing clever in the model itself is ours — Audio8 did that. This is just a careful rewrite of the inference loop around their weights, plus the profiling notes that got us there, in case they're useful to someone else pushing a small autoregressive TTS model.
RTF = autoregressive seconds / audio seconds. Lower is faster; RTF < 1 is faster than real time. Codec decode (a one-shot ~40 ms step) is reported separately and not counted in RTF — the AR loop is the throughput wall.
Measured on an RTX 5090 Laptop GPU (sm_120, ~576 GB/s), torch 2.13.0 / cu130, transformers 4.57.5, bf16, batch 1, ~150 frames/run, 3 warmup + 3 measured:
Mode (--mode) | What it is | RTF (best) | vs stock |
|---|---|---|---|
stock | stock model.generate, eager reference | ~0.54 | 1.0x |
fast-ar | stock generate, only the fast-AR codebook step compiled | ~0.27 | ~2.0x |
eager | FastArktts static rewrite, eager | ~0.56 | ~0.97x |
compile | FastArktts whole-frame torch.compile(fullgraph, dynamic=False) | ~0.12 | ~4.6x |
maxat | FastArktts whole-frame max-autotune | ~0.091 | ~5.8x |
Best: RTF 0.091 (~11x real time) with --mode maxat. Codec decode ~40 ms
(one-shot, not in RTF). VRAM peak ~2.4 GB. One-time compile cost ~47 s (compile)
/ ~190 s (maxat), cached by inductor across runs.
Note the shape of the table: the static rewrite is not faster than stock on its
own (eager is still launch-bound). Its only job is to make every tensor shape
static so the whole frame can be compiled. Compiling the whole frame is where
the win actually lands.
You need a CUDA GPU and a recent PyTorch — torch.compile drives the fast path.
pip install -r requirements.txt
One footgun worth calling out (this cost us an afternoon): pin
transformers==4.57.5 exactly, not as a floor. The model ships its modeling code
via trust_remote_code, written against the transformers 4.57.x generate/cache
API. A fresh pip/uv install will happily resolve transformers 5.x, which loads
the weights fine but then breaks the custom arktts forward/generate path in
confusing, far-from-the-error-site ways. torch, by contrast, wants to be new
(2.13 / cu130 here) — recent inductor + sm_120 support is what makes the compiled
frame fast. So: new torch, pinned transformers.
python example.py # loads public weights, compiles the frame, writes example.wav
example.py is just this:
import soundfile as sf
from fast_arktts import FastTTS
tts = FastTTS() # loads scrappylabsai/warble, compiles + warms the frame
audio, sr = tts.speak("<|speaker:2|>Thanks for calling. I can help you with that today.", seed=7)
sf.write("example.wav", audio, sr)
Zero-shot voice cloning (the stable path — give it a reference clip and its exact transcript):
audio, sr = tts.speak(
"Read this in the cloned voice.",
reference_audio="reference.wav",
reference_text="The exact transcript of reference.wav.",
)
Lower level, if you want the generator directly:
from fast_arktts import load_model, build_prompt, FastArktts
model, processor = load_model() # scrappylabsai/warble
prompt, mask = build_prompt(model, processor, "<|speaker:2|>Hello.")
fast = FastArktts(model).compile("max-autotune") # best steady-state
codes = fast.generate(prompt, mask)
wav, lens = model.decode_audio(codes)
Reproduce the table:
python benchmark.py # all modes
python benchmark.py --modes eager compile maxat --runs 5
The stock generate loop is launch-bound, not compute-bound: on the order of
~1500 tiny kernel launches per audio frame, plus dozens of blocking host→device
constant copies and a torch.cat-growing KV cache. On a fast card the GPU spends
most of each frame waiting for the next kernel to be enqueued. That's the whole
diagnosis, and it's the transferable one — before reaching for quantization or a
smaller model, check whether a small autoregressive model is starving the GPU on
launch overhead rather than saturating it.
Once you've decided the loop is launch-bound, the fix is to make the frame a
single fixed-shape region you can hand to torch.compile / capture as one CUDA
graph. Everything here is in service of that:
index_copy_ / add_ on
device counters), never reallocated, no torch.cat growth inside the loop.torch.compile(fullgraph=True, dynamic=False) (or a manual CUDA-graph capture).
It fullgraph-compiled on the first try precisely because every shape is static..item() and never forces a
per-frame host sync.The stock model modules are never modified. This code only reuses their weights through a static-shape reimplementation of the generate loop.
Each frame streams ~2.0 GB of bf16 weights (a 24-layer slow stack + 10 sequential 4-layer fast-stack calls + heads). At ~4.2 ms/frame that's ~470 GB/s effective — about 82% of this laptop 5090's ~576 GB/s memory bandwidth. The frame is memory-bound, so there's little headroom left in the AR loop itself; the next real lever is weight-only quantization (fp8/int8), not more scheduling tricks.
This is reference single-stream, batch-1 inference — one voice, one stream, as fast as we could make it on one GPU. That's the whole scope. Deliberately not included here:
Multi-stream / batched serving. The generator is written batch-general, but only B=1 is validated in this repo.
Steady-state throughput only. This release measures sustained generation speed, not first-audio latency.
Training / fine-tuning / voice-design recipes. Our voices at scrappylabsai/warble are a fine-tune of Audio8; the training side isn't in here.
A couple of honest caveats on output quality:
<|speaker:N|> no-reference line can degenerate on some random draws (the stock
reference generate does too). Pin a seed for reproducibility, or use
zero-shot cloning for stable output.scrappylabsai/warble; pass any compatible
arktts checkpoint to load_model / FastTTS.All the hard modeling work is Audio8's — the
arktts architecture, the codec, and the
Audio8-TTS-Preview-0.6b
weights this runs on, released under Apache-2.0. Thank you for shipping it openly;
this repo would not exist otherwise. If you use this, please cite and credit them.
Apache-2.0, to match the underlying Audio8 model.
Built and open-sourced by ScrappyLabs — we build our own hardware, our own render, and our own voices, and we open the reference paths when they're the kind of thing we'd have wanted to find. If this saved you an afternoon, that's the point.
4 commits
A small, standalone gpt-fast-style static-shape inference path for the public
Audio8-TTS-Preview-0.6b
text-to-speech model (the arktts architecture). It reuses the model's public
weights through its own static-shape forward code and compiles the whole
per-audio-frame step into a single CUDA graph — taking one batch-1 stream from
RTF ~0.54 (eager reference) down to ~0.091 (~11x real time) on a laptop-class
RTX 5090, which is close to the card's memory-bandwidth floor.
Nothing clever in the model itself is ours — Audio8 did that. This is just a careful rewrite of the inference loop around their weights, plus the profiling notes that got us there, in case they're useful to someone else pushing a small autoregressive TTS model.
RTF = autoregressive seconds / audio seconds. Lower is faster; RTF < 1 is faster than real time. Codec decode (a one-shot ~40 ms step) is reported separately and not counted in RTF — the AR loop is the throughput wall.
Measured on an RTX 5090 Laptop GPU (sm_120, ~576 GB/s), torch 2.13.0 / cu130, transformers 4.57.5, bf16, batch 1, ~150 frames/run, 3 warmup + 3 measured:
Mode (--mode) | What it is | RTF (best) | vs stock |
|---|---|---|---|
stock | stock model.generate, eager reference | ~0.54 | 1.0x |
fast-ar | stock generate, only the fast-AR codebook step compiled | ~0.27 | ~2.0x |
eager | FastArktts static rewrite, eager | ~0.56 | ~0.97x |
compile | FastArktts whole-frame torch.compile(fullgraph, dynamic=False) | ~0.12 | ~4.6x |
maxat | FastArktts whole-frame max-autotune | ~0.091 | ~5.8x |
Best: RTF 0.091 (~11x real time) with --mode maxat. Codec decode ~40 ms
(one-shot, not in RTF). VRAM peak ~2.4 GB. One-time compile cost ~47 s (compile)
/ ~190 s (maxat), cached by inductor across runs.
Note the shape of the table: the static rewrite is not faster than stock on its
own (eager is still launch-bound). Its only job is to make every tensor shape
static so the whole frame can be compiled. Compiling the whole frame is where
the win actually lands.
You need a CUDA GPU and a recent PyTorch — torch.compile drives the fast path.
pip install -r requirements.txt
One footgun worth calling out (this cost us an afternoon): pin
transformers==4.57.5 exactly, not as a floor. The model ships its modeling code
via trust_remote_code, written against the transformers 4.57.x generate/cache
API. A fresh pip/uv install will happily resolve transformers 5.x, which loads
the weights fine but then breaks the custom arktts forward/generate path in
confusing, far-from-the-error-site ways. torch, by contrast, wants to be new
(2.13 / cu130 here) — recent inductor + sm_120 support is what makes the compiled
frame fast. So: new torch, pinned transformers.
python example.py # loads public weights, compiles the frame, writes example.wav
example.py is just this:
import soundfile as sf
from fast_arktts import FastTTS
tts = FastTTS() # loads scrappylabsai/warble, compiles + warms the frame
audio, sr = tts.speak("<|speaker:2|>Thanks for calling. I can help you with that today.", seed=7)
sf.write("example.wav", audio, sr)
Zero-shot voice cloning (the stable path — give it a reference clip and its exact transcript):
audio, sr = tts.speak(
"Read this in the cloned voice.",
reference_audio="reference.wav",
reference_text="The exact transcript of reference.wav.",
)
Lower level, if you want the generator directly:
from fast_arktts import load_model, build_prompt, FastArktts
model, processor = load_model() # scrappylabsai/warble
prompt, mask = build_prompt(model, processor, "<|speaker:2|>Hello.")
fast = FastArktts(model).compile("max-autotune") # best steady-state
codes = fast.generate(prompt, mask)
wav, lens = model.decode_audio(codes)
Reproduce the table:
python benchmark.py # all modes
python benchmark.py --modes eager compile maxat --runs 5
The stock generate loop is launch-bound, not compute-bound: on the order of
~1500 tiny kernel launches per audio frame, plus dozens of blocking host→device
constant copies and a torch.cat-growing KV cache. On a fast card the GPU spends
most of each frame waiting for the next kernel to be enqueued. That's the whole
diagnosis, and it's the transferable one — before reaching for quantization or a
smaller model, check whether a small autoregressive model is starving the GPU on
launch overhead rather than saturating it.
Once you've decided the loop is launch-bound, the fix is to make the frame a
single fixed-shape region you can hand to torch.compile / capture as one CUDA
graph. Everything here is in service of that:
index_copy_ / add_ on
device counters), never reallocated, no torch.cat growth inside the loop.torch.compile(fullgraph=True, dynamic=False) (or a manual CUDA-graph capture).
It fullgraph-compiled on the first try precisely because every shape is static..item() and never forces a
per-frame host sync.The stock model modules are never modified. This code only reuses their weights through a static-shape reimplementation of the generate loop.
Each frame streams ~2.0 GB of bf16 weights (a 24-layer slow stack + 10 sequential 4-layer fast-stack calls + heads). At ~4.2 ms/frame that's ~470 GB/s effective — about 82% of this laptop 5090's ~576 GB/s memory bandwidth. The frame is memory-bound, so there's little headroom left in the AR loop itself; the next real lever is weight-only quantization (fp8/int8), not more scheduling tricks.
This is reference single-stream, batch-1 inference — one voice, one stream, as fast as we could make it on one GPU. That's the whole scope. Deliberately not included here:
Multi-stream / batched serving. The generator is written batch-general, but only B=1 is validated in this repo.
Steady-state throughput only. This release measures sustained generation speed, not first-audio latency.
Training / fine-tuning / voice-design recipes. Our voices at scrappylabsai/warble are a fine-tune of Audio8; the training side isn't in here.
A couple of honest caveats on output quality:
<|speaker:N|> no-reference line can degenerate on some random draws (the stock
reference generate does too). Pin a seed for reproducibility, or use
zero-shot cloning for stable output.scrappylabsai/warble; pass any compatible
arktts checkpoint to load_model / FastTTS.All the hard modeling work is Audio8's — the
arktts architecture, the codec, and the
Audio8-TTS-Preview-0.6b
weights this runs on, released under Apache-2.0. Thank you for shipping it openly;
this repo would not exist otherwise. If you use this, please cite and credit them.
Apache-2.0, to match the underlying Audio8 model.
Built and open-sourced by ScrappyLabs — we build our own hardware, our own render, and our own voices, and we open the reference paths when they're the kind of thing we'd have wanted to find. If this saved you an afternoon, that's the point.
4 commits