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_model for 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.so
    

    Copy them from $QNN_SDK_ROOT/lib/aarch64-android/ (and the V79 skel from $QNN_SDK_ROOT/lib/hexagon-v79/unsigned/) into lib/ 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.so is 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
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support