voice_audio_multi/tests/mira_tts/CPU_VULKAN_OPTIMIZATION.md
Fabio c36510c21e
Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
Initial import: audio.cpp with Orange Pi documentation
2026-11-04 19:05:22 +01:00

13 KiB
Raw Permalink Blame History

MiraTTS CPU/Vulkan optimization validation

Baseline: f3bd8255 (upstream main merged into the MiraTTS branch). Tested on Windows, September 8, 2026: Ryzen 9 7950X3D, RTX 3090 (Vulkan device 1), Release/MSVC builds, eight CPU threads.

Changes

  • CPU FlashSR: accumulate convolution taps across contiguous output positions rather than calculating one complete output sample at a time. This exposes independent samples to SIMD while retaining the input-channel/tap accumulation order for each sample, including omission of out-of-range taps. Work is still partitioned into disjoint 256-position blocks. No weight or precision changes.
  • Vulkan MiraTTS: use the existing model-local speech/EOS output-head view on Vulkan as well as CPU. The view shares the original embedding buffer and keeps its existing lifetime. It avoids projecting unused vocabulary rows, not changing the sampling alphabet, weights, or token IDs. The shared Qwen runtime is untouched. The first pass retains the existing Vulkan attention/precision settings; the later decode-only KV-view pass is documented below.
  • AUDIOCPP_MIRA_TTS_SPARSE_HEAD=0 disables the local head on CPU or Vulkan for diagnostic comparisons. CUDA retains the existing full-head behavior.

Method

Reference: assets/resources/b.wav. Models: local mira-tts-q8.gguf and mira-tts-bf16.gguf. Request parameters/seeds come from offline_requests.json and streaming_requests.json. Offline cases cover cold, repeat, changed prompt, long request, and repeat after the long request in one session. The offline token budgets cap outputs at 5.12/15.36 seconds; these are lifecycle/parity tests, not proof that the complete prompt was spoken.

Timings are per-request wall times, excluding initial model loading. Cold means the first request in a prepared session, not total process startup. Runs were serial with no competing inference. One before/after sequence per precision is reported; these are local measurements, not statistically bounded guarantees.

CPU baseline uses the original FlashSR implementation, candidate uses the new loop. Vulkan compares the full head (AUDIOCPP_MIRA_TTS_SPARSE_HEAD=0) with the local head (1). Both Vulkan runs use the same candidate binary, so the only runtime difference is the head switch.

Reproduction (change backend, device, precision, and mode as appropriate):

.\build\windows-vulkan-release\bin\mira_tts_warm_bench.exe --model ..\models_v3_test\MiraTTS-GGUF\mira-tts-q8.gguf --backend vulkan --device 1 --threads 8 --voice-ref assets/resources/b.wav --request-file tests/mira_tts/offline_requests.json --run-mode offline --model-spec-override model_specs/mira_tts.json --audio-out-dir ../outputs/candidate --summary-file ../outputs/candidate.json --log-file ../outputs/candidate.log
python tests/mira_tts/compare_optimization.py --before ../outputs/baseline.json --after ../outputs/candidate.json

Results

Backend / weights Request Before (s) After (s) Time reduction
CPU Q8 clone_cold 8.245 7.446 9.7%
CPU Q8 clone_repeat 8.210 7.445 9.3%
CPU Q8 short_second_prompt 8.211 7.407 9.8%
CPU Q8 longform 26.923 24.106 10.5%
CPU Q8 clone_repeat_after_longform 8.180 7.457 8.8%
CPU Q8 stream_cold 32.852 28.893 12.0%
CPU Q8 stream_repeat 33.243 28.826 13.3%
CPU BF16 clone_cold 8.933 8.121 9.1%
CPU BF16 clone_repeat 8.851 8.170 7.7%
CPU BF16 short_second_prompt 8.832 8.488 3.9%
CPU BF16 longform 28.677 26.416 7.9%
CPU BF16 clone_repeat_after_longform 8.767 8.383 4.4%
CPU BF16 stream_cold 36.138 33.739 6.6%
CPU BF16 stream_repeat 36.528 32.801 10.2%
Vulkan Q8 clone_cold 0.994 0.940 5.5%
Vulkan Q8 clone_repeat 0.881 0.815 7.5%
Vulkan Q8 short_second_prompt 0.930 0.829 10.8%
Vulkan Q8 longform 2.942 2.743 6.8%
Vulkan Q8 clone_repeat_after_longform 0.937 0.881 6.0%
Vulkan Q8 stream_cold 3.747 3.444 8.1%
Vulkan Q8 stream_repeat 3.739 3.432 8.2%
Vulkan BF16 clone_cold 1.015 1.065 -4.9%
Vulkan BF16 clone_repeat 0.928 0.857 7.7%
Vulkan BF16 short_second_prompt 0.951 0.863 9.3%
Vulkan BF16 longform 3.173 2.898 8.7%
Vulkan BF16 clone_repeat_after_longform 1.017 0.999 1.8%
Vulkan BF16 stream_cold 3.973 3.687 7.2%
Vulkan BF16 stream_repeat 3.969 3.681 7.3%

All 28 paired requests have identical complete WAV bytes (SHA-256), reported audio hashes, frame counts, sample formats, durations, and event counts. validate_bench.py passes for all four backend/precision combinations, including repeat-after-long-form and streaming repeat determinism. Every streaming request produced four audio events. Cross-backend/precision outputs are not expected to be identical; each candidate is compared only to its matching baseline.

The BF16 Vulkan cold-request regression in this sequence illustrates startup variability; warm requests and streaming improved. No universal cold-start speedup is claimed.

An additional short-prompt Q8 CPU check ("Hello, this is a native Mira TTS test.", seed 1234, max_tokens 1024, same reference and eight threads) produced 6.18 seconds of identical audio: repeated-request wall time 10.222 -> 8.994 s, and FlashSR stage time 2.085 -> 1.110 s. This is separate from the capped fixtures in the table.

flashsr_utility_test passes its two Python-reference fixtures plus eight input length cases (1, 7, 85, 86, 255, 256, 257, 1025) checking finite output, 3x length, and exact one/eight-thread agreement around convolution block boundaries. The six no-model tests for compare_optimization.py also pass, including rejection of changed WAV bytes despite an unchanged reported hash.

Local artifacts: ../outputs/perf-cpu-{q8,bf16}-{before,after}-{offline,streaming} and ../outputs/perf-vk-{q8,bf16}-{0,1}-{offline,streaming}, with sibling .json summaries and .log traces. Large WAV/model files are not added to the repository.

Experiments not adopted

  • Raising CPU thread count from 4 to 16 improved elapsed time, but changed the generated hash and duration under otherwise identical sampling parameters. It is not an exact-output optimization; the default thread count is unchanged.
  • CPU graph rebuilding costs only a few milliseconds versus seconds in generation/decoding. Its repeat-determinism safeguard is retained.
  • No lower precision, changed sampling defaults, shortened token budgets, or reduced audio quality settings were introduced.

Second CPU pass: local accumulator and pointer-based convolution

The local comparison tree (TEST_22_audio.cpp_qwen_3.8, src/community_models/miratts/dsp_internal.h) uses a small stack accumulator and pointer-based convolution loops to help MSVC vectorize. This pattern is adapted to FlashSR's residual convolutions and final output convolution, rather than replacing MiraTTS's generator/decoder wholesale. Each tile is written to the output once. The input-channel/tap summation order and final tanhf are unchanged. Empty tap intersections are rejected before forming input pointers.

The short-prompt Q8 comparison was rerun against a saved first-pass binary on the same machine: repeated request 9.005 -> 8.671 s (3.7% less time), FlashSR 1.115 -> 0.637 s (42.8% less time), identical complete WAV. The generator's runtime varied slightly between runs, so stage and total improvements differ.

Additional experiments were rejected and removed:

  • Packing only speech/EOS weight rows: same audio, but CPU slowed slightly and Vulkan's warm improvement was only about 1.5%, insufficient to justify an extra copied weight allocation in this pass.
  • Skipping odd/noncontributing upsampling taps with a variable-bound loop: preserved audio but did not improve measured performance.
  • Restricting the eight-thread benchmark process to either 16-logical-CPU group: slower than the unrestricted process. No affinity settings persist, and no affinity/default-thread-count changes are included.
CPU weights Request First pass (s) Second pass (s) Time reduction
Q8 clone_cold 7.446 7.018 5.7%
Q8 clone_repeat 7.445 7.035 5.5%
Q8 short_second_prompt 7.407 7.082 4.4%
Q8 longform 24.106 22.381 7.2%
Q8 clone_repeat_after_longform 7.457 7.016 5.9%
Q8 stream_cold 28.893 28.053 2.9%
Q8 stream_repeat 28.826 27.965 3.0%
BF16 clone_cold 8.121 7.550 7.0%
BF16 clone_repeat 8.170 7.553 7.6%
BF16 short_second_prompt 8.488 7.644 9.9%
BF16 longform 26.416 23.891 9.6%
BF16 clone_repeat_after_longform 8.383 7.504 10.5%
BF16 stream_cold 33.739 31.376 7.0%
BF16 stream_repeat 32.801 31.435 4.2%

All 14 second-pass sequence outputs are byte-identical to their first-pass counterparts; both precisions pass the offline/streaming lifecycle validator. The FlashSR reference/boundary tests and comparison-tool unit tests also pass. The first-pass sequence timings were collected earlier on the same machine; the fresh short-prompt comparison above corroborates the stage-level benefit. These are local observations, not multi-trial confidence intervals.

Artifacts: ../outputs/round2-cpu-{q8,bf16}-{offline,streaming} and their sibling JSON/log files. CPU/Vulkan CLI and server targets were rebuilt. There is no additional Vulkan algorithm change in this pass; its earlier optimization is retained. The alternate source tree was inspected read-only.

Third pass: Vulkan decode-only KV views

Vulkan now uses FlashGroupedViewKV for single-token decoding, avoiding materialization of grouped cached heads on each token. Prefill remains FlashGrouped: the previous strided-prefill precision issue is not bypassed. F32 projection precision, sampling, token budgets and CPU execution are unchanged. Set AUDIOCPP_MIRA_TTS_VULKAN_VIEW_DECODE=0 to compare the previous decode path.

RTX 3090, Vulkan device 1, eight host threads, same fixtures as above:

Weights Request Before (s) After (s) Time reduction
Q8 clone_repeat 0.816 0.792 2.9%
Q8 longform 2.787 2.674 4.1%
Q8 stream_repeat 3.395 3.424 -0.9%
BF16 clone_repeat 0.853 0.824 3.3%
BF16 longform 2.977 2.731 8.3%
BF16 stream_repeat 3.735 3.551 4.9%

All 14 paired offline/streaming WAV files are byte-identical, and both lifecycle validators pass. Q8 streaming did not improve in this measurement; the change does not claim universal speedups. Other Vulkan devices are not validated here.

The initial BF16 offline runs had abnormal first-request times (61–65 seconds) in both modes. Their cause is unconfirmed. These timings are excluded from the table; a fresh paired rerun returned to normal timings. Artifacts are retained for inspection rather than treating the anomaly as an optimization gain.

Artifacts: ../outputs/round3-vk-{q8,bf16}-{0,1}-{offline,streaming}; the BF16 offline table uses ../outputs/round3-rerun-bf16-{0,1}.json. The final default build is additionally exercised in ../outputs/round3-final-{q8,bf16}.json. Use compare_optimization.py with the corresponding before/after JSON paths.

CPU pool experiment rejected

An opt-in persistent ggml thread pool was tested in an off/on/on/off sequence. Repeated short-request timings were 8.747 / 8.551 / 8.748 / 8.632 seconds, with identical WAVs. This does not establish a repeatable improvement, so the pool code was removed. The current build uses OpenMP, which already reuses OS worker threads; allocating a ggml pool per graph is not equivalent to creating new OS threads per token. The accepted CPU convolution optimizations remain intact.

Fourth pass: no additional optimization retained

The following experiments were removed rather than claiming noise as a gain:

  • CPU direct-convolution tiles enlarged from 256 to 1024: repeated short Q8 request 8.684 -> 8.660 s, cold request 8.699 -> 8.722 s. Inconclusive.
  • CPU activation downsampling tiled across positions with the same tap order: repeated request 8.739 s, slower than the 8.684 s baseline.
  • Channel-major direct interpolation was tried, but initially benchmarked on Vulkan, which uses the GGML upsampler rather than this CPU DSP function. Those measurements do not validate the CPU optimization; it was removed.

The CPU tile and downsampling WAVs match the baseline byte-for-byte; FlashSR utility tests pass. Artifacts: ../outputs/round4-cpu-tile-* and ../outputs/round4-cpu-downsample.*. The Vulkan round4-interp-* and round4-tile-* measurements are retained only as diagnostic artifacts, not evidence for a speedup. Previous accepted optimizations remain unchanged.