- 共享引擎(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 模板
183 lines
10 KiB
Markdown
183 lines
10 KiB
Markdown
# 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.
|