Stable Audio 3 (Small-SFX + Small-Music) β ExecuTorch / Qualcomm QNN β SM8750
On-device textβaudio for Stable Audio 3 Small-SFX, lowered to the ExecuTorch QNN backend and validated end-to-end on a Snapdragon 8 Elite (SM8750, HTP V79) phone. The diffusion denoiser runs on the NPU; the text encoder and audio decoder run on CPU (XNNPACK).
Prompt β 16 s stereo 44.1 kHz WAV. The included constants reproduce the demo prompt "a dog barking".
These are runtime artifacts (compiled .pte graphs + a prebuilt aarch64 runner), not
trainable checkpoints. Source weights derive from
Stability-AI/stable-audio-3; see
License below.
Pipeline
prompt ββΊ T5Gemma encoder ββΊ cross_attn_cond (1,257,768) + global_cond (1,768)
β
init noise (1,256,174) ββΊ 8Γ ping-pong diffusion, each step = DiT(x, Ο)
β
final latent ββΊ SAME-S decoder ββΊ stereo 44.1 kHz audio
L = latent length = audio_samples / 4096. SFX 10 s (+headroom) β L = 174 β 16 s.
The DiT is CFG-free at batch 1 (distilled rf_denoiser).
| Stage | Backend | File | Validation (cos vs FP32) |
|---|---|---|---|
| T5Gemma encoder + conditioning | XNNPACK CPU, FP16 | sa3_cond.pte (563 MB) |
0.99997 |
| DiT denoiser (run 8Γ) | QNN / HTP, FP16 | sa3_dit.pte (940 MB) |
0.9984, ~50 ms/step |
| SAME-S decoder (Conv1dβConv2d) | XNNPACK CPU, FP32 | sa3_same_decoder.pte (218 MB) |
1.0000 |
Performance (SM8750, warm)
The SAME-S decoder's lone Conv1d is exported as a 2D convolution (height 1) so
XNNPACK delegates and multithreads it instead of falling back to a single-threaded
scalar kernel. This is bit-exact (eager max|Ξ| = 0.0; a seed-matched on-device A/B
gives cosine 1.000000000, max |Ξ| = 1 LSB = β90 dBFS) and ~3.4Γ faster decode.
End-to-end for the 16 s SFX clip: ~6 s total (model load + conditioning ~2.3 s, 8 DiT steps on the NPU ~0.4 s, decode ~2 s). For reference, an FP16 decoder is a size win only β it runs ~3Γ slower (no fast FP16 conv microkernels on the Oryon CPU).
Contents
sa3_cond.pte # text encoder + conditioning (XNNPACK, FP16)
sa3_dit.pte # diffusion denoiser (QNN/HTP, FP16) β run 8Γ
sa3_same_decoder.pte # latent β audio decoder (XNNPACK, FP32)
sa3_runner # prebuilt aarch64 ExecuTorch runner (gflags CLI)
consts/
input_ids.bin # tokenized prompt "a dog barking" (256 Γ int64)
attn.bin # prompt attention mask (256 Γ bool)
lac.bin padext.bin mem.bin freqs.bin # L=174 constants (local_add_cond,
# externalized pad mask, memory tokens, RoPE freqs)
lib/
libqnn_executorch_backend.so # ExecuTorch QNN backend (BSD)
libc++_shared.so # NDK C++ runtime
input_ids.bin / attn.bin encode the prompt. To synthesize a different prompt,
re-tokenize with the T5Gemma (Gemma SentencePiece) tokenizer on a host and overwrite
these two blobs (256-length, left/right per the model). lac/padext/mem/freqs.bin are
L-dependent constants and are reused for any L=174 prompt.
Requirements
A Snapdragon 8 Elite / SM8750 device (HTP V79). Other SoCs need the DiT re-lowered with
--soc_modelfor that HTP version.The 5 Qualcomm QNN runtime libraries (proprietary β not redistributed here), from the Qualcomm QNN SDK 2.37+:
libQnnHtp.so libQnnHtpPrepare.so libQnnHtpV79Skel.so libQnnHtpV79Stub.so libQnnSystem.soCopy them from
$QNN_SDK_ROOT/lib/aarch64-android/(and the V79 skel from$QNN_SDK_ROOT/lib/hexagon-v79/unsigned/) intolib/next to the shipped.so.
Run on device
# push (one transfer; never run concurrent adb pushes)
adb shell mkdir -p /data/local/tmp/sa3
adb push sa3_cond.pte sa3_dit.pte sa3_same_decoder.pte sa3_runner /data/local/tmp/sa3/
adb push consts lib /data/local/tmp/sa3/
# add the 5 Qualcomm .so to lib/ (see Requirements), then:
adb push libQnnHtp.so libQnnHtpPrepare.so libQnnHtpV79Skel.so \
libQnnHtpV79Stub.so libQnnSystem.so /data/local/tmp/sa3/lib/
adb shell '
cd /data/local/tmp/sa3
chmod +x sa3_runner
LD_LIBRARY_PATH=$PWD/lib ADSP_LIBRARY_PATH=$PWD/lib \
./sa3_runner --cond sa3_cond.pte --dit sa3_dit.pte \
--decoder sa3_same_decoder.pte --consts consts \
--output sa3_out.wav --seed 0'
adb pull /data/local/tmp/sa3/sa3_out.wav .
The runner: loads the conditioning, runs the 8-step ping-pong sampler (fixed Ο schedule
for L=174, baked in), decodes, and writes a stereo WAV. Flags: --L, --seed,
--sample_rate, --output. The sampler uses its own RNG for the ping-pong noise, so
output differs sample-wise run to run (expected for a stochastic sampler).
Music (Small-Music) β bonus bundle
The same pipeline runs Stable Audio 3 Small-Music
(stabilityai/stable-audio-3-small-music), which is architecturally identical to
SFX β same DiT/decoder config, only the weights differ. The music artifacts live
under music/ with a longer latent length L = 388 (a 30 s prompt β 36 s canvas).
Reuse the same sa3_runner and lib/ from the repo root (the Ο schedule is
identical to SFX and baked in; --L 388 sizes the latent and constants).
music/
sa3_cond_music.pte # text encoder + conditioning (small-music weights)
dit_music.pte # diffusion denoiser (QNN/HTP, FP16) β run 8x
sa3_same_decoder_L388.pte # latent -> audio decoder (Conv1d->Conv2d, L=388)
consts/ # L=388 constants + tokenized "lo-fi hip hop beat, 90 BPM"
adb shell mkdir -p /data/local/tmp/sa3/music
adb push music/sa3_cond_music.pte music/dit_music.pte \
music/sa3_same_decoder_L388.pte /data/local/tmp/sa3/music/
adb push music/consts /data/local/tmp/sa3/music/
adb shell '
cd /data/local/tmp/sa3
LD_LIBRARY_PATH=$PWD/lib ADSP_LIBRARY_PATH=$PWD/lib \
./sa3_runner --cond music/sa3_cond_music.pte --dit music/dit_music.pte \
--decoder music/sa3_same_decoder_L388.pte --consts music/consts \
--L 388 --output music_out.wav --seed 0'
adb pull /data/local/tmp/sa3/music_out.wav .
Generates 36 s of stereo music in ~8 s on device (conditioning ~2.3 s, 8 DiT steps on the NPU ~1 s, decode ~4.5 s).
Why the QNN DiT works (the non-obvious part)
A first lowering ran fully on HTP and fast, but produced garbage (cos 0.36 vs FP32).
It was not a precision problem β eager all-FP16 matched the golden at 0.99997. The
bug was in the QNN lowering: the self-attention key-padding mask, built in-graph as a
static constant (aten.full(bool)) concatenated with a bool input, is miscomputed on
HTP. Fix: precompute that mask as a float graph input (padext.bin) and inject it,
so the broken in-graph build never runs β 0.36 β 0.998 at full FP16 speed. Memory
tokens, RoPE freqs, and the Fourier timestep table are externalized the same way.
License
- Model weights derive from Stability AI's Stable Audio 3 and are governed by the Stability AI Community License. Review it before any commercial or redistribution use; this repo is a research/educational port.
libqnn_executorch_backend.sois part of ExecuTorch (BSD-3).- Qualcomm QNN libraries are not included β obtain them under your own QNN SDK license terms.
Ported with Claude Code. Companion: the OmniVoice QNN port. Build host AOT-compiles on x86-64 Linux (QNN SDK β₯ 2.37); runner cross-compiled for Android arm64.
- Downloads last month
- 48