Repository navigation
BreezeTTS: opt-in fused depth graph for CUDA (perf_mode=cuda_perf) - #1090
Conversation
On CUDA, the 15 depth-decoder codebook steps of a frame now run as a single ggml graph with an in-graph KV state and on-device top-k/top-p Gumbel-max sampling, instead of one graph launch and host round trip per codebook. The semantic sampler's torch RNG offsets are advanced as the per-step path would, so the semantic stream is unchanged. The offline path now prefills the backbone straight into the decode cache, as the streaming path already does since 0xShug0#858, which removes the per-request KV export/import. On CUDA the backbone and depth graphs are also kept between requests, and the whole-cache bf16 re-rounding after each cache update is skipped when the K/V rows are already bf16-rounded, the cache is F16 and flash attention is used (an identity there; output is bit-identical). Other backends keep the per-codebook depth path, the reference rounding and the per-request graph release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@Fishballllllll Thanks for the PR! I'm actually backporting optimizations right now: VoxCPM2 (#1047), Qwen3-TTS (#1055), YuE2 (#1056), Drama3 (#1068), ACE-Step (#1073), MiniMax Music3 (#1077). Please follow the same patterns and add an option Changing defaults can unexpectedly break configurations that currently work. Main's defaults are at least compatibility baseline. I've changed optimization defaults before and quickly received regression reports (#1016 #1071). |
Add breeze_tts.perf_mode=off|cuda_perf with breeze_tts.fused_depth and breeze_tts.keep_decode_graphs, following the fish_drama3 and minimax_music3 controls. Both default to off, so CUDA keeps the per-codebook depth path and releases decode graphs after each request unless the preset or an explicit option enables them. The fused graph now uses flash attention for the codebooks that attend over its in-graph K/V, like the per-codebook path's cached decode, and embeds from the device token embedding when it holds the f32 table's exact values instead of uploading a second f32 copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Feed the fused graph the per-codebook sampler's own torch exponential draws, indexed by token, so cuda_perf picks the same tokens as the per-codebook path for the same seed. Skip the per-codebook depth runtimes when the fused graph replaces them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop keep_decode_graphs. Keeping the backbone and depth graphs resident between requests held the largest request's buffers and raised peak VRAM by about 7% for at most a few percent of speed, so cuda_perf now enables only the fused depth graph and frees the graphs like main does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Offline direct prefill and skipping the identity bf16 re-rounding of the F16 KV cache were always on. They do not change output, but they change the default execution path, so make them opt-in like fused_depth: breeze_tts.direct_prefill and breeze_tts.skip_cache_round, both false by default and enabled by perf_mode=cuda_perf, CUDA only. With them off, offline prefill goes through the same export/import path as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
perf_mode=cuda_perf)
|
@0xShug0 Thanks for testing it and for pointing me to the other
The PR description has the full numbers and how I measured them. To try it: If it's useful, I can also mention |
|
@Fishballllllll I've decided to include all three of your PRs in the next release, so if any issues come up, I'll just fix them directly. |
|
@Fishballllllll Thanks! PR merged. The updated PR works great! Since the parity and VRAM concerns have been addressed, I’ll run more WER checks and may enable it by default before the release. |
|
@0xShug0 Thanks so much for merging it and for considering it as a default! And thanks for building audio.cpp and for the careful review. Working on this PR taught me a lot. I’m just getting started with open source and GitHub, so this was a really valuable first experience. If your WER checks or any other testing turn up a problem, just ping me and I’m happy to fix it. Beyond these PRs, I’d also love to help with other work on the project to keep learning, so if there’s anything you’d like a hand with, feel free to point me to it. I also have a few more ideas for BreezeTTS that might make cuda_perf another 20–30% faster (a rough, untested estimate). If you think it’s worth it, I can work on them over the next few days and send a follow-up PR, in time for this release if it hasn’t gone out yet, or for the next one otherwise. I’m also rebasing #1091 and #1092 onto the latest main to resolve the conflicts, and will push the updated branches shortly. |
|
@Fishballllllll You've done a great job, especially for someone just getting started with open source. Really glad to have you contributing to audio.cpp! The BreezeTTS optimization ideas sound interesting. Looking forward to the follow-up PR! No rush for the next release, which is planned for tomorrow... Depending on the scope of the changes, we can work toward a more systematic way to integrate optimizations that aren’t model-local. For example, we introduced |
…d staging) + Earshot VAD + BreezeTTS CUDA opts + Echo-TTS CFG + /health version Upstream commits 899d84a..71f42b8 (22): - fix HIP VMM access updates for growing pools (0xShug0#1059) - gguf: numeric array metadata in one bulk fread (0xShug0#1072) - Echo-TTS optional Joint-CFG (0xShug0#773); Sway time scheduling later - validated HIP VMM defaults in Arch AMD installer (0xShug0#1080) - HIP argsort extended to large rows (0xShug0#1076) - Irodori-TTS MeanFlow support (0xShug0#1083) - assets: direct uploads preserved in weight-norm tensor views (0xShug0#1081) - HIP device weight uploads staged through bounded host buffer (0xShug0#1070) - empty/null speech voice values treated as omitted (0xShug0#1086) - redundant VoxCPM2 KV cache staging/reset avoided (0xShug0#1085) - Drama3 device sampling default off (0xShug0#1087) - Studio LoRA controls + shared adapter groups (0xShug0#1088) - Arena styling alignment (0xShug0#1089) - Earshot VAD native C++ port (0xShug0#1095) - ASR VAD chunking loads any built-in VAD family (0xShug0#1096) - /health reports build version + git commit (0xShug0#1097) - BreezeTTS opt-in fused depth graph CUDA (0xShug0#1090); bf16 rounding fusion in rms_norm/add/SwiGLU (0xShug0#1091); batch-2 CFG decode (0xShug0#1092); option names normalized - ci: best-effort HIP test release binaries (0xShug0#1100) - Echo-TTS Sway time scheduling (0xShug0#1105)
Summary
breeze_tts.perf_mode=off|cuda_perf(defaultoff), following the Drama3 / MiniMax Music3 pattern, with three individual options it presets:breeze_tts.fused_depth,breeze_tts.direct_prefillandbreeze_tts.skip_cache_round(all defaultfalse). Explicit options override the preset; other backends ignore them.off, BreezeTTS runs the same code path as onmain: output is byte-identical, and speed and peak VRAM matchmain.cuda_perf, CUDA output is also byte-identical tomainfor the same seed, peak VRAM is no higher thanmain's, and offline RTF is 1.82x faster at CFG 4 (0.802 → 0.442) and 1.70x faster at CFG 1 (0.577 → 0.340).Problem
On CUDA, most of a BreezeTTS frame was spent outside the actual GPU work:
breeze_tts.ar.start_decode_ms) took 460–680 ms per request. The streaming path has prefilled straight into the decode cache since Improve Qwen3 and Breeze TTS streaming and Breeze longform voice consistency #858.Change
All model code is in
src/models/breeze_tts/(generator.cpp,session.cpp), plusinclude/engine/models/breeze_tts/generator.h,model_specs/breeze_tts.jsonanddocs/models/breeze_tts.md. Each of the three options below is off unless set explicitly or throughcuda_perf, and takes effect on CUDA only.breeze_tts.fused_depth:BreezeFusedDepthRuntimebuilds the whole depth loop of a frame as one fixed-shape ggml graph. The K/V of earlier codebooks are carried inside the graph, and the CFG mix, temperature, top-k/top-p and sampling run on the device. Each frame is one graph launch plus a download of 15 tokens. It matches the per-codebook path exactly:DecoderPrefixAttentionMode::FlashWithPrefix).torch_cuda_tensor_iterator_exponential_element_at_offsetat the same seed and RNG offsets, uploads-log(E)per token, and the graph picksargmax(logits / T - log E). The torch RNG offsets advance exactly as before.breeze_tts.direct_prefill: offline generation prefills into the decode cache, as streaming already does. This is the same computation without the host round trip. With it off, offline prefill goes through the same export/import calls as onmain.breeze_tts.skip_cache_round: with the bf16 reference path, an F16 cache and flash attention, K/V rows are already bf16-rounded before they are written, and an F16 cache stores those values exactly. Re-rounding the whole cache after each update is therefore an identity, and this option skips it.What changed since your review
Thanks for testing it. Both trade-offs you saw were real, and I could reproduce them once I measured like you did: at the default
guidance_scale1, and with peak VRAM sampled finely (my earlier 200 msnvidia-smisampling missed the peaks).perf_mode, off by default. That includes direct prefill and the cache-round skip, which were unconditional in the first revision (and direct prefill applied to every backend); they are now separate options, CUDA only.std::mt19937Gumbel noise instead of the torch-CUDA stream. Both are fixed as described above.cuda_perfnow reproducesmainbyte for byte, so accuracy is unchanged by construction.mainon the longest requests. About half came from keeping the decode graphs resident between requests. That saved at most ~2.5% RTF, so I removed it: graphs are released after each request, as onmain. The other half came from the fused graph itself: a second 256 MB F32 copy of the audio embedding table, and the per-codebook depth runtimes that were still created alongside it. Both are gone. Peak VRAM withcuda_perfis now within 0.4% ofmainon the longest requests and belowmainon the 10-request set.Validation
Setup: RTX 5090 Laptop GPU (24 GB, sm_120, driver 616.92), WSL2 Ubuntu 24.04, CUDA 12.8, conda-forge g++ 13.4.
main(2f6b42d) and this branch (based on efdf97e) built with the same compiler and flags. The two commits between them (#1085, #1086) do not change BreezeTTS: #1085 adds a KV-cache import option that only VoxCPM2 enables.Requests:
audiocpp_cli --task tts --family breeze_tts --backend cuda --request-sequence <file> --metrics, one warm-up request excluded. The 10-request set is Mandarin instruction-controlled voice cloning (one 6 s reference clip, seed 1234, default sampling,guidance_scale1 or 4). The long set is the 8 longest of a 47-request set (32–93 characters,guidance_scale4). Warm RTF is sum(wall_ms) / sum(audio_duration_ms). Peak VRAM is the device's used memory sampled through NVML about every 0.2 ms over the whole process, including loading and warm-up.Speed, peak VRAM and byte identity (Q8_0, 10 requests, all runs back to back)
mainoff(default)cuda_perfmainmainoffruns the same code asmain; the CFG 1 difference is within this laptop's run-to-run variation (mainitself measured 0.572–0.594 at CFG 1 across runs the same day).Further byte-identity checks with
cuda_perfThese were run on earlier commits of this branch that compute the same thing with
cuda_perf(the later commits only release the decode graphs after each request and move direct prefill and the cache-round skip behind options):Peak VRAM on the long set (Q8_0, CFG 4)
mainmaincuda_perfNot tested
main; I did not rebuild or run them for this revision.BackendType::Cudain audio.cpp, so the options would also take effect there, but I have no MUSA device.cuda_perf, and Windows builds.model_spec_system_testandattention_fallback_test, in the CUDA build, and both pass; I did not run the full ctest.🤖 Generated with Claude Code