Files
SoundGame/tools/stable-audio-3/optimized/tensorRT/quantize/README.md
T
SoundGame Dev f0f0a9b50e 初始提交:3 款纯音频游戏 + 共享引擎 + 完整文档与资产管线
- 共享引擎(app/src/main):GestureEngine 6 手势识别、AudioEngine 三层音频
  (ExoPlayer BGM 交叉淡入 / 对白 ducking / SoundPool 低延迟)、Game/GameEngine
  状态机接口、MainActivity 全屏封面 + TalkBack
- 《声命线》storyline:末世互动广播剧,3 幕 8 抉择点 4 结局
- 《声东击西》reflex:听声辨位反应,12 波战斗 + BGM 三档 BPM 变速
- 《暗室》escape:3x3 网格密室逃脱,钥匙→铁柜→保险丝→电箱→大门谜题链
- 文档:setup(从零搭建)/ architecture(架构)/ engine-api(引擎 API)/
  asset-pipeline(资产流水线)/ games/*(三款游戏设计文档)
- 资产管线:Qwen3-TTS 对白 + 程序合成音效/BGM + Mureka BGM 的 gen 脚本与台账,
  全部 ogg 资产入库可复现
- env.d 环境脚本:android/tts 入库;sfx/mureka 含密钥不入库,仅提交 .example.sh 模板
2026-08-29 13:00:34 +08:00

183 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SA3 Encoder + Decoder — Quantized Tiers (SAME-S / SAME-L)
Train-free quantized variants of the Stable Audio 3 **encoders and decoders**, each a single
**self-contained ONNX** derived from the published bf16 export. TensorRT builds the engine; quality is
preserved by GPTQ weight-error compensation and per-op precision placement (details below).
Two tiers, both derived from the published bf16 export:
- **`fp8`** — fp8 **compute** on the FFN GEMMs (fp8 tensor cores) → real speedup **and** a smaller engine, near-transparent.
- **`fp8_fast`** — fp8 pushed into the attention projections too (SAME-S only) — faster still, lower quality.
For max fidelity, use the bf16 baseline (or `--precision fp32`).
> **Retired — `w8_bf16` (int8 weight-only).** Its `DequantizeLinear` constant-folds to bf16 at build, so
> the *engine* came out byte-for-byte the size and speed of the bf16 baseline (only the *ONNX* was
> smaller) with slightly lossier weights — strictly dominated by bf16. The lesson worth keeping: **ONNX
> size ≠ engine size.** fp8 stays 1-byte all the way through (small ONNX *and* small engine *and* fp8
> math); an int8-weight-only tier only shrinks the download, then expands back to bf16 in the engine.
---
## Tiers at a glance
The **bf16 baseline is the first row** of each table. Decoder dB is PSNR vs that bf16 baseline; encoder
dB/cos is vs **eager** encode (the latent). dB is a heuristic — confirm near-transparent tiers by ear;
`_fast` has audible artifacts. Speed is whole-model median at seq 1292 (~2 min audio).
### SAME-S decoder
| file | quantized | compute | onnx | speed | dB vs bf16 (music / sfx) |
|---|---|---|---|---|---|
| `dec_dynamic_bf16.onnx` | — *(bf16 baseline)* | bf16 | 219 MB | 1.00× | ref (7.25 ms @1292 · 23.2 @4096) |
| `dec_fp8.onnx` | fp8 weights + activations (FFN) | fp8 FFN GEMM | 58 MB | **1.14×** | 30.9 / 41.0 · near-transparent |
| `dec_fp8_fast.onnx` | fp8 weights + activations (all linears) | fp8 all GEMMs | 58 MB | **1.22×** | 26.1 / 36.5 · lossy |
### SAME-L decoder
| file | quantized | compute | onnx | speed | dB vs bf16 |
|---|---|---|---|---|---|
| `dec_dynamic_triton_swa.onnx` | — *(bf16 baseline)* | bf16 | 1193 MB | 1.00× | ref (54.0 ms @1292 · 174.4 @4096) |
| `dec_fp8.onnx` | fp8 weights + activations (FFN) | fp8 FFN GEMM | 937 MB | **1.15×** | 43.4 · near-transparent |
SAME-L has **no `_fast` tier**: its attention projections are fp32 islands (fp8 there collapses quality — see islands below).
### SAME-S encoder
The **encoder is cheap** (~1 ms SAME-S, ~7 ms SAME-L), so quant mostly buys **size** — `fp8` shrinks both the onnx and the engine.
| file | quantized | compute | onnx | speed | dB / cos vs eager |
|---|---|---|---|---|---|
| `enc_dynamic_bf16.onnx` | — *(bf16 baseline)* | bf16 | 216 MB | 1.00× | 36.4 / 0.997 (~1 ms) |
| `enc_fp8.onnx` | fp8 (FFN) | fp8 FFN GEMM | 54 MB | 1.08× | 29.9 / 0.986 · near-transparent |
| `enc_fp8_fast.onnx` | fp8 (all linears) | fp8 all GEMMs | 54 MB | 1.14× | 22.3 / 0.924 · lossy |
### SAME-L encoder
| file | quantized | compute | onnx | speed | dB / cos vs eager |
|---|---|---|---|---|---|
| `enc_dynamic_triton_swa.onnx` | — *(bf16 baseline)* | bf16 | 1192 MB | 1.00× | 46.0 / 0.9997 (~7 ms) |
| `enc_fp8.onnx` | fp8 (FFN) | fp8 FFN GEMM | 895 MB | 1.03× | 30.8 / 0.989 · near-transparent |
Encoder input is audio `(1,2,N)`; output latent `(1,256,N/4096)`. **Every encoder onnx carries a
silence-pad node** (see below), so any `N` is accepted.
### `fp8` vs `fp8_fast`
Both run the **FFN** GEMMs (`ff.0` / `ff.2`) in fp8. The difference is the **attention projections**:
- **`fp8`** leaves `to_qkv` / `to_out` in bf16 compute (weight-only fp8 for storage) — attention inputs
stay full-precision → near-transparent.
- **`fp8_fast`** runs `to_qkv` / `to_out` in fp8 too — the extra **+0.08×** (1.14 → 1.22×), but it
fp8-rounds Q/K/V *before* the still-bf16 attention core, and the softmax path is fp8-sensitive → the
quality drop (31 → 26 dB). SAME-S only.
---
## Sequence length, padding & building
The onnx graphs are **fully dynamic** — every model is correct at **any** length (verified L=1→8192
vs eager, odd/even/prime, no size-dependent error; quant tiers track bf16 identically at all lengths).
The only bound is the **TensorRT optimization profile** you build with. Two rules:
- **Build wide.** Use `min=1` and a `max` covering your longest track. Reference build uses
**latent `[1, 1292, 8192]`** (decoders, ≈12:40) and **audio `[1, 2097152, 33554432]`** (encoders).
`L=1–31` then work and match eager (these can't be chunked); long files run **natively, no chunking**
(a 12:40 decode / 6:20 encode is a single call). The old `[32,4096]` was just a conservative default.
- **Encoders auto-pad.** The SAME-L encoder needs the audio length to be a multiple of 4096 (the
downsample ratio); the built-in pad node silence-pads the tail up to the next multiple, matching what
eager does internally. Callers feed any `N`. (SAME-S never needed it; padded anyway for a uniform API.)
The `encode_chunked` path in the runtime is a **stale workaround** — it predated wide profiles and was
chasing what turned out to be out-of-profile garbage, not a real divergence. Single-shot is accurate at
any length (SAME-L enc cos 0.9995 on a real 285 s track). Chunking is unnecessary.
---
## Where the quantization goes, per tier
Every decoder block is `latent_proj → [ attn: to_qkv → (QKᵀ·softmax·V) → to_out ] → [ ff: ff.0 → GELU → ff.2 ]`,
with RMSNorm + RoPE around attention. Each tier touches a different subset:
### `dec_fp8` — fp8 on the FFN
- **FFN** (`ff.0` up-proj, `ff.2` down-proj) `+ latent_proj`: **weight fp8 + activation fp8** (per-tensor clipping scale) → **fp8 GEMM**. Weights GPTQ-compensated.
- **Attention projections** (`to_qkv`, `to_out`): SAME-S → **weight-only fp8** (bf16 compute — kept out of fp8 math because they feed softmax); SAME-L → **fp32 islands** (untouched).
- **Attention core** (QKᵀ·softmax·V): bf16 flash (unchanged).
- **RMSNorm / RoPE:** fp32.
### `dec_fp8_fast` — fp8 everywhere it pays (SAME-S only)
- **All linears** (FFN + `to_qkv` + `to_out` + `latent_proj`): **weight fp8 + activation fp8** → fp8 GEMM.
- **Attention core** still bf16 — but now it's fed **fp8-rounded Q/K/V** from the projections. *That rounding is the quality cost* (the softmax path is fp8-sensitive), buying +0.08× speed over `dec_fp8`.
- **RMSNorm / RoPE:** fp32.
---
## The levers (reference)
### What you quantize — the two halves of every GEMM
| lever | format | compute or storage | speed | quality | notes |
|---|---|---|---|---|---|
| **Weights** | fp8 (per-tensor*) or int8 (per-channel) | **storage** (dequant→bf16 at build) | none | near-lossless; **GPTQ** compensates rounding | 1 byte either format — fp8 ≠ smaller than int8 |
| **Activations** | fp8 only (dynamic, per-tensor **clip** scale) | turns the GEMM into fp8 tensor-core compute | **unlocks the speedup** | the **lossy half** (~31 dB floor; GPTQ can't fix it) | calibration-critical — amax scale → collapse; use a clipping scale |
\* fp8 weight scale **must be per-tensor in a strongly-typed build** (per-channel silently collapses the GEMM). int8 is happily per-channel. int8 *activations* are a non-starter — softmax breaks.
### Where you apply it — which op
| lever | touches | fp8? | speed | quality | verdict |
|---|---|---|---|---|---|
| **FFN GEMM** | `ff.0`, `ff.2` | ✅ weight+act | 1.14× (both) / 1.07× (ff.0 only) | ff.0 transparent; **ff.2 sensitive** (±outliers) | the main speed win → `dec_fp8` |
| **Attn projections** | `to_qkv`, `to_out` | ✅ weight+act | +0.08× on top | lossy — fp8-rounds Q/K/V *before* attention | `dec_fp8_fast` (SAME-S) |
| **Attn core** | QKᵀ·softmax·V | ❌ no fp8 MHA kernel @ hd=64 | ~none | — | **dead at hd=64** (Sage 0.46–0.97×, fully-fp8 softmax slower); the real lever is algorithmic **windowing** (~1.6× on SAME-L long-seq), not quant |
### Leave-alone islands (constraints, not levers)
| op | rule | why |
|---|---|---|
| **RMSNorm** | keep fp32/bf16 | cheap; quantizing buys nothing |
| **RoPE** | **must stay fp32** | bf16 RoPE angle → long-sequence clip bug (≥2 min renders) |
| **Attn projections (SAME-L)** | fp32 islands | fp8 compute there collapses quality in the strongly-typed graph |
The mental model: **activations = speed (and the quality floor); weights = size (near-free with GPTQ); attn core = neither (use windowing).**
---
## Which to pick
- **Max fidelity** → the bf16 baseline (or `--precision fp32` for bit-exact eager)
- **Faster + smaller, near-transparent (default quant)** → `dec_fp8`
- **Fastest, quality-tolerant** → `dec_fp8_fast` (SAME-S)
## Reducing SAME-L size (optional)
Both SAME-L tiers are 937 MB because the attention projections stay fp32 (680 MB). Weight-only-quantizing
those islands (int8/fp8 weights, bf16 compute — no speed or quality cost) pulls both to ~500 MB.
## Building the engines
The tier **ONNX files ship on HuggingFace** (`stabilityai/stable-audio-3-optimized`, alongside the
existing `dec_dynamic_bf16.onnx` / `enc_dynamic_*`). To build an engine with the wide profile:
```bash
python build_tiers.py <tier.onnx> <out.trt> --arch {same-s|same-l} --kind {enc|dec} [--fp8]
# SAME-S fp8 / fp8_fast tiers need --fp8; SAME-L carries fp8 in-graph (strongly-typed) and needs no flag.
```
`build_tiers.py` bakes the **wide** profile (decoder `latent [1,1292,8192]`, encoder `audio [1,·,33.5M]`),
so L=1..31 work and long files run natively. SAME-L uses the `diff_attn_swa` plugin (PREFER_JIT).
## Reproducing the tier ONNX (grafters)
The quantized ONNX are produced by grafting QDQ / int8 / fp8-stored weights onto the published bf16 export:
| grafter | produces |
|---|---|
| `fp8_gptq.py` | SAME-S `dec_fp8` / `dec_fp8_fast` (fp8-stored + GPTQ, ORT Hessian capture) |
| `fp8_gptq_samel.py` | SAME-L `dec_fp8` (fp8-stored + GPTQ, eager Hessian capture — SWA blocks ORT) |
| `gptq_w8.py` / `gptq_samel.py` | int8 weight-only ONNX — the *retired* `w8_bf16` tier (kept for reference) |
| `enc_quant_sames.py` / `enc_quant_samel.py` | the encoder tiers |
| `pad_encoder.py` | adds the silence-pad node so any audio length is accepted |
> **Note:** the grafters reference campaign-local calibration data (real-audio latents) and model
> checkpoints via absolute paths at the top of each file — adjust those to reproduce. Calibration +
> Hessians are computed from real-audio latents. The ready-made ONNX on HF need none of this.