- 共享引擎(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 模板
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). ItsDequantizeLinearconstant-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:
fp8leavesto_qkv/to_outin bf16 compute (weight-only fp8 for storage) — attention inputs stay full-precision → near-transparent.fp8_fastrunsto_qkv/to_outin 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=1and amaxcovering your longest track. Reference build uses latent[1, 1292, 8192](decoders, ≈12:40) and audio[1, 2097152, 33554432](encoders).L=1–31then 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.0up-proj,ff.2down-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 fp32for 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:
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.