File size: 9,237 Bytes
a05f2d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# Stable Audio 3 β€” TensorRT engines

Prebuilt TensorRT engines for the Stable Audio 3 stack, organised by CUDA architecture.

```
tensorRT/sm_90/     Hopper   β€” H100, H200
tensorRT/sm_120/    Blackwell β€” RTX 5090, RTX PRO 6000, RTX PRO 4500
```

TensorRT engines are **not portable across architectures** β€” an `sm_90` engine will not
load on an `sm_120` GPU. They are also tied to the TensorRT version they were built with
(these were built with **TensorRT 10.15.1.29**). If your GPU or TensorRT version is not
covered here, build from the ONNX in `onnx/` using the scripts in
[`optimized/tensorRT/build/`](https://github.com/Stability-AI/stable-audio-3/tree/main/optimized/tensorRT/build).

| directory | contents |
|---|---|
| `sa3-m/` | medium DiT (the main diffusion transformer) |
| `sa3-sm-music/`, `sa3-sm-sfx/` | small domain-specialised DiTs |
| `same-l/`, `same-s/` | autoencoder (SAME) encoder + decoder |
| `t5gemma/` | text conditioner, plus its tokenizer |

---

## The SAME-L attention kernel: two implementations

The SAME-L encoder and decoder use a custom sliding-window-attention plugin
(`samel::diff_attn_swa`) for differential attention, which TensorRT has no native op for.
That plugin ships **two implementations**, and *which one an engine uses is fixed when the
engine is built* β€” it is baked into the `.trt` file and cannot be changed at load time.

**AOT** (ahead-of-time) compiles a block-tiled tensor-core kernel to PTX and embeds it in
the engine. No Python runs during inference. **JIT** dispatches a Triton kernel through a
Python callback on every enqueue β€” 12 callbacks per decode, one per attention layer.

| | AOT | JIT |
|---|---|---|
| CUDA-graph capturable | **yes, both architectures** | sm_90 only β€” **fails on sm_120** |
| Python / Triton calls per decode | **0** | 12 |
| Triton needed at inference | no | **yes** |
| Engine size | +0.1% | baseline |

### Which files use which

| published engines | kernel | why |
|---|---|---|
| `sm_120/same-l/` | **AOT** (block-tiled MMA) | required β€” the JIT build is not capturable on sm_120 |
| `sm_90/same-l/` | **JIT** | works, and AOT measured no faster on Hopper, so these were left alone |

So on **sm_90 you need Triton installed to run** the `same-l` engines, and on **sm_120 you
do not**. Both implementations stay registered by the runtime, so either kind of engine
loads and runs regardless of which GPU you are on.

For anything you build yourself, **AOT is the default on every architecture** β€” it is the
only correct choice on sm_120, it is at parity on sm_90, and it drops the runtime Triton
dependency. Rebuilding the `sm_90` engines would therefore give you AOT rather than the
JIT files published here; that is expected and fine, not a mismatch.

### Why JIT breaks on sm_120

The runtime captures the whole pipeline into one CUDA graph. A JIT engine cannot be
captured on sm_120: `enqueueV3` returns `False`, TensorRT logs *"this TRT engine is not
stream capturable"*, and **the decode stage is silently omitted from the graph** β€” no
exception, no non-zero exit.

What you get is the pre-capture warm-up decode of zero latents: a constant wash of noise,
**byte-identical for every prompt and every seed**, with exit code 0. If you ever see
that, this is the first thing to check. The diagnostic is to render twice with different
seeds and compare the files β€” identical bytes mean the decode never ran.

The same engine captures without complaint on sm_90, which is why the problem did not
surface until Blackwell. Re-entering Python inside a captured region was always fragile
rather than supported, so do not assume a future architecture will tolerate it either.

The decode is inside the captured region on the default path: `sa3_trt.py` captures T5 β†’
DiT loop β†’ decoder β†’ PCM copy as one graph whenever `--cfg 1.0` and no inpaint/init-audio
is used, which is the default. `--no-mega-graph` runs eagerly and sidesteps capture
entirely, so it is a usable workaround on an affected engine β€” at the cost of the
graph-replay speedup.

> **The `sm_120` SAME-L engines were rebuilt AOT on 2026-07-31.** Anything published
> before that date was a JIT build and is affected, so if you have an `sm_120` copy cached
> from earlier, re-pull it. The `sm_90` files are unchanged β€” JIT captures correctly on
> Hopper, so they were never affected.

### Benchmarks

SAME-L decode, milliseconds, median of 7 on an otherwise idle GPU. `L` is the latent
sequence length β€” one latent is 4096 samples, so at 44.1 kHz L=1292 is a two-minute
render and L=4096 is about 6m20s.

**sm_120 Β· RTX PRO 4500 Blackwell**

| kernel | L=256 | L=1024 | L=1292 | L=2048 | L=4096 |
|---|---|---|---|---|---|
| JIT *(not capturable here)* | 55.1 | 227.2 | 289.1 | 457.7 | 920.2 |
| AOT `ptx` (scalar fallback) | 62.5 | 251.2 | 318.7 | 502.8 | 1008.8 |
| **AOT `mma` (default)** | 55.4 | **223.6** | **283.7** | **448.4** | **899.3** |

**sm_90 Β· H200**

| kernel | L=256 | L=1024 | L=1292 | L=2048 | L=4096 |
|---|---|---|---|---|---|
| JIT | 12.4 | 47.0 | 61.1 | 98.5 | 194.4 |
| AOT `ptx` (scalar fallback) | 12.7 | 47.3 | 60.7 | 96.6 | 195.5 |
| **AOT `mma` (default)** | 12.4 | 48.4 | 62.6 | 100.0 | 196.8 |

The default AOT kernel is at parity with JIT on both architectures β€” within a few percent
either way, and faster at long sequence lengths on sm_120 β€” while remaining capturable.
Accuracy against the FP32 decoder at L=1292: AOT `mma` **51.45 dB** PSNR, JIT 51.43, AOT
`ptx` 51.11. The default kernel is the most accurate of the three as well as the fastest.

Note the ~4.5Γ— gap between the two architectures. That is silicon, not a software
regression β€” a workstation card against a flagship datacenter part. Measured on these two
GPUs, the H200 has **5.57Γ—** the copy bandwidth (4084 vs 734 GB/s) and 5.08Γ— the BF16
tensor throughput, so a bandwidth-bound decoder landing at 4.5Γ— is extracting slightly
*more* of its silicon on sm_120 than the H200 does of its own.

### How the kernels differ

The default AOT kernel (`mma`) is block-tiled: 16 queries per block share a 64-wide K/V
tile in shared memory, and both attention products go through `tl.dot`, which lowers to
512 Γ— `mma.sync.aligned.m16n8k8.row.col.f32.tf32.tf32.f32` β€” TF32 tensor cores. It is
written in Triton and compiled ahead of time, so Triton generates the fragment layouts
rather than us hand-writing them, but the result is a plain PTX blob inside the engine.

The fallback AOT kernel (`ptx`) is hand-written scalar FP32: one warp per query, no K/V
reuse, zero `mma` instructions. It exists only for targets where the block-tiled kernel
will not build, and costs about 10% on sm_120.

TF32 in the two attention products matches what the JIT path already did β€” it is not a new
precision loss. The differential subtraction stays in FP32 on FP32 accumulators, so the
cancellation-sensitive step is untouched.

### Building your own

```bash
SA3_SWA_PLUGIN=aot   # default β€” PTX compiled into the engine
SA3_SWA_PLUGIN=jit   # Triton through a Python callback per enqueue
SA3_SWA_AOT=mma      # default AOT kernel β€” block-tiled, tensor cores
SA3_SWA_AOT=ptx      # fallback AOT kernel β€” scalar FP32
```

Triton is needed to *build* an AOT engine (it is the code generator) but not to run one.

Three gotchas, all of which produce wrong output rather than a build failure:

- **Kernel parameter order is inputs β†’ runtime scalars β†’ outputs.** TensorRT's AOT
  launcher passes the `extra` scalars *before* the output pointers. Declaring the output
  ahead of them makes the kernel dereference a sequence length as an address; ours died
  with an illegal memory access until the order was fixed.
- **`BLOCK_KV` must be β‰₯ `BLOCK_N + 2*WINDOW`.** Otherwise the K/V tile stops covering
  every position the block's queries can attend to and attention contributions are
  *silently dropped*. `BLOCK_N=32` with `BLOCK_KV=64` needs 66 and is therefore wrong,
  even though it compiles and fits in shared memory.
- **Shared memory over 48 KB fails at enqueue, not at build.** `BLOCK_N=64` needs 64 KB;
  the engine builds cleanly and then reports *"Failed to enqueue status -1"* and returns
  zeros. `BLOCK_N=16` needs 40 KB, and is faster anyway β€” with a window of only Β±17, a
  wide K/V tile is mostly masked-out waste.

The engine filenames still say `triton_swa` because Triton remains the code generator;
only the compilation moved ahead of time.

---

## Other things worth knowing

**The medium DiT has three precisions.** `dit_fp16mixed.trt` is the default and the one to
use: FP16 attention core with FP32 RMSNorm and RoPE islands, ~4.3Γ— faster than FP32 with
no audible quality cost. `dit_fp32.trt` is the reference. `dit_bf16.trt` is kept for
comparison β€” earlier builds of it degraded past about two minutes of audio because the
RoPE angle was computed in BF16, where one ULP exceeds 2Ο€ at long sequence lengths; the
published engine has the RoPE table baked in at FP32 precision.

**`dit_fp8.trt` is sm_90 only** and must be built weakly-typed; strong typing breaks
TensorRT's attention fusion. This is the *opposite* of the FP16-mixed rule, which requires
`STRONGLY_TYPED` or the builder re-casts the FP32 islands.