Skip to content

Commit 217e853

Browse files
authored
bf16 medium DiT: FMHA-fused TRT engine build recipe + CLI (bf16 = medium default, fp16-mixed kept) (#73)
1 parent 17ba505 commit 217e853

5 files changed

Lines changed: 163 additions & 28 deletions

File tree

optimized/tensorRT/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,35 @@ package install + a ~5 GB engine download).
117117
Omit `--dit` / `--decoder` for an interactive arrow-key picker. Relative
118118
`--out` paths land in `output/`; absolute paths are honoured as-is.
119119

120+
### DiT precision (`--precision`)
121+
122+
The default resolves per model — no flag needed for the recommended setup:
123+
124+
| model | default | why |
125+
|------------------|-------------|------------------------------------------------------------|
126+
| `medium` | `bf16` | FMHA-fused (0 → 96 fused attention nodes) → **1.8× @256 / 4.7× @4096** vs fp16-mixed, within the perceptual floor |
127+
| `sm-music`/`sm-sfx` | `fp16mixed` | standard attention — already fuses in fp16-mixed |
128+
129+
`--precision` also takes `fp16mixed` and `fp32` explicitly:
130+
131+
- **`bf16`***medium only.* Same `dit.onnx` as fp32, built with `BuilderFlag.BF16`;
132+
bf16 carries fp32's range so the FP32-softmax islands vanish and TRT's FMHA fuser
133+
fires. **Not seed-reproducible vs fp16-mixed** — the medium DiT uses *differential*
134+
attention (cancellation-sensitive), so bf16 is a different-but-equal take per seed
135+
(quality within the re-seed floor; exact samples differ). Same varlen profile
136+
(L 1..4096, opt 1292) and full mode/feature set as the other precisions.
137+
- **`fp16mixed`** — canonical FP16 trunk + FP32 islands. Bit-reproducible; use it when
138+
you need exact per-seed reproducibility or max per-step fidelity.
139+
- **`fp32`** — pure FP32, bit-equivalent to PyTorch eager (~2× slower, ~2× VRAM).
140+
141+
```bash
142+
# medium defaults to bf16 (fast); force the reproducible engine instead:
143+
./sa3 --prompt "..." --dit medium --decoder same-l --precision fp16mixed
144+
```
145+
146+
The TRT DiT engines are static batch=1 (the ONNX bakes batch=1), so CFG runs as a
147+
sequential cond+uncond dual-pass at batch=1 for every precision.
148+
120149
## Speed & memory
121150

122151
Measured on **H100 SXM 80 GB** at `--steps 8` (rf-denoiser sweet spot).

optimized/tensorRT/build/build.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,13 @@ def _from_onnx(name):
6767
"outputs": ["same-l/dec_dynamic_triton_swa.trt"]},
6868
# DiT engines now build from pre-processed FP16-mixed ONNX on HF;
6969
# build_from_onnx.py does the simple STRONGLY_TYPED compile.
70-
{"label": "DiT medium (SA3-M, FP16-mixed)",
70+
# Medium ships TWO engines: bf16 (FMHA-fused, the speed DEFAULT) built from
71+
# the raw fp32 dit.onnx with BuilderFlag.BF16, and fp16-mixed (canonical,
72+
# bit-reproducible, kept selectable). sm-music/sm-sfx are fp16-mixed only.
73+
{"label": "DiT medium (SA3-M, bf16 — FMHA-fused, medium DEFAULT)",
74+
"command": _from_onnx("sa3-m-bf16"),
75+
"outputs": ["sa3-m/dit_bf16.trt"]},
76+
{"label": "DiT medium (SA3-M, FP16-mixed — selectable, bit-reproducible)",
7177
"command": _from_onnx("sa3-m"),
7278
"outputs": ["sa3-m/dit_fp16mixed.trt"]},
7379
{"label": "DiT sm-music (FP16-mixed)",

optimized/tensorRT/build/build_from_onnx.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,33 @@
141141
"profile": _DIT_PROFILE,
142142
"plugin": False,
143143
},
144+
# SA3 medium DiT in bf16 — the medium SPEED DEFAULT (fp16-mixed above stays
145+
# selectable). Reuses the SAME raw fp32 dit.onnx as the fp32 variant, but
146+
# builds with BuilderFlag.BF16 + EXPLICIT_BATCH (NOT STRONGLY_TYPED): bf16
147+
# carries fp32's dynamic range, so the FP32-softmax islands that fp16-mixed
148+
# kept vanish and TRT 10.15's FMHA fuser fires (0 → 96 fused _gemm_mha_v2
149+
# nodes) → measured ~1.76×@L=256 / 4.70×@L=4096 vs fp16-mixed, within the
150+
# perceptual re-seed floor (FAD 0.59× floor, CLAP within spread, n=128).
151+
# It is a build-time PRECISION RECIPE only — no new ONNX, no graph change.
152+
# medium-only: sm-music / sm-sfx use standard attention and already fuse in
153+
# their fp16-mixed engines. bf16 is NOT seed-reproducible vs fp16-mixed
154+
# (differential attention is cancellation-sensitive) — a different-but-equal
155+
# take per seed; use fp16-mixed for bit-reproducibility.
156+
# Same _DIT_PROFILE (batch=1, dynamic L∈[1,4096], opt=1292) as every DiT
157+
# engine → identical CLI/feature surface (varlen, CFG sequential dual-pass,
158+
# neg-prompt/APG, a2a, inpaint). The DiT ONNX bakes batch=1, so there is no
159+
# varbatch axis on ANY TRT DiT engine (CFG is a sequential dual-pass, as in
160+
# fp16-mixed) — fusion is verified to survive the full L profile at batch=1.
161+
"sa3-m-bf16": {
162+
# 5.8 GB external-data sidecar (fp32 weights) travels alongside.
163+
"onnx_hf": ["sa3-m/dit.onnx", "sa3-m/dit.onnx.data"],
164+
"trt_local": "sa3-m/dit_bf16.trt",
165+
"flags": {"BF16"},
166+
"network": "EXPLICIT_BATCH",
167+
"workspace_gb": 16,
168+
"profile": _DIT_PROFILE,
169+
"plugin": False,
170+
},
144171
# ── FP32 variants ────────────────────────────────────────────────────
145172
# DiT FP32: read the unsurgered FP32 ONNX directly (dit.onnx), build
146173
# STRONGLY_TYPED. ~2× the engine size of FP16-mixed, ~2× slower, but

optimized/tensorRT/scripts/sa3_trt.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ class SA3Inference:
393393
DEFAULT_SIGMA_MAX = 1.0 # mega-graph path requires this
394394

395395
def __init__(self, dit: str, decoder: str, *,
396-
precision: str = "fp16mixed",
396+
precision: str | None = None,
397397
default_T_lat: int = 324, default_steps: int = 8,
398398
default_seconds: float = 30.0,
399399
models_dir: Path | None = None,
@@ -404,9 +404,13 @@ def __init__(self, dit: str, decoder: str, *,
404404
Args:
405405
dit: one of DIT_CHOICES — "sm-music" / "sm-sfx" / "medium"
406406
decoder: one of DECODER_PATHS — "same-s" / "same-l"
407-
precision: "fp16mixed" (default, fastest) or "fp32" (bit-equiv
408-
PyTorch eager, ~2× slower). Engines auto-download
409-
from HF if the requested precision file is missing.
407+
precision: None (default → per model: "bf16" for medium, else
408+
"fp16mixed"), or an explicit "bf16" (medium only;
409+
FMHA-fused, ~1.8-4.7× faster, within the perceptual
410+
floor, not seed-reproducible vs fp16mixed),
411+
"fp16mixed" (canonical, bit-reproducible), or "fp32"
412+
(bit-equiv PyTorch eager, ~2× slower). Engines auto-
413+
download from HF if the requested file is missing.
410414
default_T_lat: latent length to build the initial graph at
411415
default_steps: pingpong steps for the initial graph
412416
default_seconds: duration condition for the initial graph (used for
@@ -420,8 +424,15 @@ def __init__(self, dit: str, decoder: str, *,
420424
raise ValueError(f"unknown dit={dit!r}; valid: {list(DIT_CHOICES)}")
421425
if decoder not in DECODER_PATHS:
422426
raise ValueError(f"unknown decoder={decoder!r}; valid: {list(DECODER_PATHS)}")
427+
# Resolve precision default per model: bf16 for medium (FMHA-fused speed
428+
# default), fp16-mixed for sm-music/sm-sfx. bf16 is medium-only.
429+
if precision is None:
430+
precision = canon.default_precision(dit)
423431
if precision not in canon.PRECISIONS:
424432
raise ValueError(f"unknown precision={precision!r}; valid: {canon.PRECISIONS}")
433+
if precision == "bf16" and dit != "medium":
434+
raise ValueError("precision='bf16' is only available for dit='medium' "
435+
"(sm-music/sm-sfx already fuse in fp16mixed)")
425436

426437
# Quiet: patch canon's stage/sub/_stage_vram to no-ops so loading
427438
# doesn't spam stdout (gradio in particular wants a clean log).
@@ -633,9 +644,12 @@ def main():
633644
ap.add_argument("--inpaint-range", default=None)
634645
ap.add_argument("--dit", choices=list(DIT_CHOICES.keys()), default=None)
635646
ap.add_argument("--decoder", choices=list(DECODER_PATHS.keys()), default=None)
636-
ap.add_argument("--precision", choices=list(canon.PRECISIONS), default="fp16mixed",
637-
help="Engine precision: 'fp16mixed' (default, fast) or 'fp32' "
638-
"(bit-equiv PyTorch eager, slower). Auto-downloads from HF.")
647+
ap.add_argument("--precision", choices=list(canon.PRECISIONS), default=None,
648+
help="DiT engine precision. Default resolves per model: 'bf16' for medium "
649+
"(FMHA-fused, ~1.8-4.7× faster, within perceptual floor; not "
650+
"seed-reproducible vs fp16mixed), 'fp16mixed' for sm-music/sm-sfx. "
651+
"'fp16mixed' = canonical/bit-reproducible. 'fp32' = bit-equiv PyTorch "
652+
"eager, slower. bf16 is medium-only. Auto-downloads from HF.")
639653
ap.add_argument("--models-dir", default=str(canon.MODELS_DIR))
640654
ap.add_argument("--seconds", type=float, default=30.0)
641655
ap.add_argument("--steps", type=int, default=8)

optimized/tensorRT/scripts/sa3_trt_core.py

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def _detect_gpu_arch() -> str:
103103
DIT_ENGINE_FILES = {
104104
"sm-music": ["sa3-sm-music/dit_fp16mixed.trt"],
105105
"sm-sfx": ["sa3-sm-sfx/dit_fp16mixed.trt"],
106-
"medium": ["sa3-m/dit_fp16mixed.trt"],
106+
"medium": ["sa3-m/dit_bf16.trt"], # bf16 (FMHA-fused) is the medium default
107107
}
108108
DECODER_FILES = {
109109
"same-s": [
@@ -129,7 +129,7 @@ def _detect_gpu_arch() -> str:
129129
"default_decoder": "same-s"},
130130
"sm-sfx": {"engine": ARCH_DIR / "sa3-sm-sfx" / "dit_fp16mixed.trt",
131131
"default_decoder": "same-s"},
132-
"medium": {"engine": ARCH_DIR / "sa3-m" / "dit_fp16mixed.trt",
132+
"medium": {"engine": ARCH_DIR / "sa3-m" / "dit_bf16.trt", # bf16 = medium default
133133
"default_decoder": "same-l"},
134134
}
135135
DECODER_PATHS = {
@@ -144,51 +144,94 @@ def _detect_gpu_arch() -> str:
144144

145145
# ─── Precision-keyed engine maps ─────────────────────────────────────────
146146
#
147-
# The canonical engines are FP16-mixed (FP16 trunk + FP32 islands around
148-
# RMSNorm / Softmax / RoPE). Pure-FP32 variants are also published — same
149-
# numerical behavior as PyTorch eager FP32. Use `--precision fp32` on the
150-
# CLI to pick them; default is `fp16mixed`.
147+
# Three DiT precisions:
148+
# bf16 — medium ONLY. Same dit.onnx as fp32, built with BuilderFlag.BF16
149+
# (EXPLICIT_BATCH). bf16 carries fp32's range, so the FP32-softmax
150+
# islands vanish and TRT 10.15's FMHA fuser fires (0 → 96 fused
151+
# _gemm_mha_v2 nodes) → 1.76×@256 / 4.70×@4096 vs fp16-mixed,
152+
# within the perceptual re-seed floor (FAD 0.59× floor, n=128).
153+
# NOT seed-reproducible vs fp16-mixed (differential attention is
154+
# cancellation-sensitive → a different-but-equal take per seed).
155+
# THE MEDIUM DEFAULT. (sm-music/sm-sfx use standard attention and
156+
# already fuse in fp16-mixed — no bf16 engine for them.)
157+
# fp16mixed — canonical (FP16 trunk + FP32 islands around RMSNorm/Softmax/
158+
# RoPE). Kept selectable for bit-reproducibility / max per-step
159+
# fidelity. The sm-music / sm-sfx default.
160+
# fp32 — pure-FP32, bit-equivalent to PyTorch eager. ~2× size/latency.
151161
#
152162
# The lookup tables below resolve the engine filename per (dit/decoder,
153-
# precision). Encoders are FP16-mixed only.
163+
# precision). The bf16 DiT recipe is a build-time precision change only (no new
164+
# ONNX): reuse sa3-m/dit.onnx, build with BF16. Decoders/encoders are unchanged
165+
# by bf16 (it's a DiT-trunk fusion recipe), so decoder "bf16" reuses the
166+
# canonical decoder engine. Encoders are FP16-mixed only.
154167
DIT_ENGINE_FILENAME = {
168+
"bf16": "dit_bf16.trt", # medium only (FMHA-fused; medium default)
155169
"fp16mixed": "dit_fp16mixed.trt",
156170
"fp32": "dit_fp32.trt",
157171
}
172+
# DiT precisions actually built per model. bf16 is medium-only.
173+
_DIT_PRECISIONS = {
174+
"sm-music": ("fp16mixed", "fp32"),
175+
"sm-sfx": ("fp16mixed", "fp32"),
176+
"medium": ("bf16", "fp16mixed", "fp32"),
177+
}
178+
# Per-DiT default precision (bf16 for medium, fp16mixed elsewhere).
179+
DIT_DEFAULT_PRECISION = {"sm-music": "fp16mixed", "sm-sfx": "fp16mixed", "medium": "bf16"}
158180
_DIT_SUBDIR = {"sm-music": "sa3-sm-music", "sm-sfx": "sa3-sm-sfx", "medium": "sa3-m"}
159181
DECODER_ENGINE_FILENAME = {
160182
"same-l": {
183+
# bf16 is a DiT-only recipe → decoder reuses its canonical fp16-mixed engine.
184+
"bf16": "dec_dynamic_triton_swa.trt",
161185
"fp16mixed": "dec_dynamic_triton_swa.trt",
162186
"fp32": "dec_dynamic_fp32.trt",
163187
},
164188
"same-s": {
189+
"bf16": "dec_dynamic_bf16.trt",
165190
"fp16mixed": "dec_dynamic_bf16.trt",
166191
"fp32": "dec_dynamic_fp32.trt",
167192
},
168193
}
169-
PRECISIONS = ("fp16mixed", "fp32")
194+
PRECISIONS = ("bf16", "fp16mixed", "fp32")
195+
196+
197+
def default_precision(dit_name: str) -> str:
198+
"""Default DiT precision for a model: bf16 for medium (FMHA-fused speed
199+
default), fp16-mixed otherwise."""
200+
return DIT_DEFAULT_PRECISION.get(dit_name, "fp16mixed")
170201

171202

172-
def get_dit_engine_path(dit_name: str, precision: str = "fp16mixed") -> Path:
203+
def get_dit_engine_path(dit_name: str, precision: str = None) -> Path:
173204
if dit_name not in _DIT_SUBDIR:
174205
raise ValueError(f"unknown dit={dit_name!r}; valid: {list(_DIT_SUBDIR)}")
206+
if precision is None:
207+
precision = default_precision(dit_name)
175208
if precision not in DIT_ENGINE_FILENAME:
176209
raise ValueError(f"unknown precision={precision!r}; valid: {PRECISIONS}")
210+
if precision == "bf16" and dit_name != "medium":
211+
raise ValueError(
212+
f"precision='bf16' is only available for --dit medium (FMHA-fused); "
213+
f"{dit_name} uses standard attention and already fuses in fp16mixed. "
214+
f"Valid for {dit_name}: {_DIT_PRECISIONS.get(dit_name)}")
177215
return ARCH_DIR / _DIT_SUBDIR[dit_name] / DIT_ENGINE_FILENAME[precision]
178216

179217

180-
def get_decoder_engine_path(decoder_name: str, precision: str = "fp16mixed") -> Path:
218+
def get_decoder_engine_path(decoder_name: str, precision: str = None) -> Path:
181219
if decoder_name not in DECODER_ENGINE_FILENAME:
182220
raise ValueError(f"unknown decoder={decoder_name!r}; valid: {list(DECODER_ENGINE_FILENAME)}")
221+
if precision is None:
222+
precision = "fp16mixed" # decoders have no bf16-specific engine; canonical
183223
if precision not in DECODER_ENGINE_FILENAME[decoder_name]:
184224
raise ValueError(f"unknown precision={precision!r}; valid: {PRECISIONS}")
185225
return ARCH_DIR / decoder_name / DECODER_ENGINE_FILENAME[decoder_name][precision]
186226

187227

188-
def get_engine_files(dit_name: str, decoder_name: str, precision: str = "fp16mixed",
228+
def get_engine_files(dit_name: str, decoder_name: str, precision: str = None,
189229
with_encoder: bool = False) -> list[str]:
190230
"""Relative paths (under ARCH_DIR) needed for the chosen pipeline. Pass this
191-
list to _ensure_files() to auto-download anything missing from HF."""
231+
list to _ensure_files() to auto-download anything missing from HF.
232+
precision=None resolves to the per-model default (bf16 for medium)."""
233+
if precision is None:
234+
precision = default_precision(dit_name)
192235
files = list(SHARED_FILES)
193236
files.append(f"{_DIT_SUBDIR[dit_name]}/{DIT_ENGINE_FILENAME[precision]}")
194237
files.append(f"{decoder_name}/{DECODER_ENGINE_FILENAME[decoder_name][precision]}")
@@ -903,14 +946,22 @@ def _arrow_pick(prompt: str, options: list[str], default: str | None = None) ->
903946

904947

905948
def prompt_user_if_missing(args):
906-
"""Fill in --dit / --decoder / --seed interactively if missing."""
949+
"""Fill in --dit / --decoder / --precision / --seed interactively if missing."""
907950
if args.dit is None:
908951
args.dit = _arrow_pick("Choose DiT model:", list(DIT_CHOICES.keys()), default="medium")
909952
print(f" → {args.dit}")
910953
if args.decoder is None:
911954
suggested = DIT_CHOICES[args.dit]["default_decoder"]
912955
args.decoder = _arrow_pick("Choose audio decoder:", list(DECODER_PATHS.keys()), default=suggested)
913956
print(f" → {args.decoder}")
957+
# Resolve DiT precision default per model: bf16 for medium (FMHA-fused speed
958+
# default), fp16-mixed for sm-music/sm-sfx. bf16 is medium-only (sm-music/
959+
# sm-sfx use standard attention and already fuse in fp16mixed).
960+
if getattr(args, "precision", None) is None:
961+
args.precision = default_precision(args.dit)
962+
if args.precision == "bf16" and args.dit != "medium":
963+
sys.exit("error: --precision bf16 is only available for --dit medium "
964+
"(sm-music / sm-sfx already fuse in fp16mixed).")
914965
if args.seed is None:
915966
args.seed = random.randint(0, 2**31 - 1)
916967
return args
@@ -958,10 +1009,14 @@ def main():
9581009
ap.add_argument("--decoder", choices=list(DECODER_PATHS.keys()), default=None,
9591010
help="Audio decoder. 'same-s' pairs with sm-* (110 MB engine). "
9601011
"'same-l' pairs with medium (1.2 GB engine). Interactive picker if omitted.")
961-
ap.add_argument("--precision", choices=list(PRECISIONS), default="fp16mixed",
962-
help="Engine precision. 'fp16mixed' (default) = FP16 trunk + FP32 islands, "
963-
"fastest. 'fp32' = pure FP32, matches PyTorch eager bit-for-bit but ~2× "
964-
"slower and ~2× the VRAM. Engines auto-download from HF if missing.")
1012+
ap.add_argument("--precision", choices=list(PRECISIONS), default=None,
1013+
help="DiT engine precision (default resolves per model: 'bf16' for medium, "
1014+
"'fp16mixed' for sm-music/sm-sfx). 'bf16' (MEDIUM ONLY) = FMHA-fused, "
1015+
"~1.8-4.7× faster than fp16mixed, within the perceptual floor, but not "
1016+
"seed-reproducible vs fp16mixed (differential attention). 'fp16mixed' = "
1017+
"FP16 trunk + FP32 islands (canonical, bit-reproducible). 'fp32' = pure "
1018+
"FP32, matches PyTorch eager bit-for-bit but ~2× slower and ~2× the VRAM. "
1019+
"Engines auto-download from HF if missing.")
9651020
ap.add_argument("--models-dir", default=str(MODELS_DIR),
9661021
help=f"Directory containing the TRT engines. Default: {MODELS_DIR}")
9671022
# ── Sampling ──
@@ -1165,8 +1220,12 @@ def _stage_vram(label): return 0
11651220
_w_m = torch.zeros(1, T5_MAX_LEN, device="cuda")
11661221
_w_l = torch.zeros(1, 257, T_lat, device="cuda")
11671222
_w_lat = torch.zeros(1, IO_CHANNELS, T_lat, device="cuda")
1168-
_w_audio = torch.zeros(1, 2, T_lat * SAMPLES_PER_LATENT, device="cuda") \
1169-
if "enc" in runners else None
1223+
# Encoder warmup is at the chunk_lat shape that encode_chunked actually
1224+
# uses; encoding at the full T_lat shape diverges past T_lat≈100-200 on
1225+
# both encoders (see encoder_encode docstring), so we never feed the
1226+
# engine that shape — encode_chunked stitches chunk_lat=50 windows.
1227+
_w_audio = torch.zeros(1, 2, DEFAULT_ENCODER_CHUNK_LAT * SAMPLES_PER_LATENT,
1228+
device="cuda") if "enc" in runners else None
11701229
# Optional: pre-allocate a pinned-memory destination buffer for the
11711230
# Stage-5 narrow + DtoH path. With pinned dst + non_blocking=True the DMA
11721231
# goes straight from GPU into RAM without the usual pageable→pinned
@@ -1275,7 +1334,7 @@ def _stage_vram(label): return 0
12751334
audio_t = torch.from_numpy(audio_np).unsqueeze(0).cuda() # (1, 2, T)
12761335
sub(f"read+prep ({init_action}) {(time.time() - t0) * 1000:.0f} ms")
12771336
t0 = time.time()
1278-
init_latents = encoder_encode(runners["enc"], audio_t)
1337+
init_latents = encode_chunked(runners["enc"], audio_t)
12791338
sub(f"encode {(time.time() - t0) * 1000:.0f} ms latents {tuple(init_latents.shape)}")
12801339
if args.free_models:
12811340
runners["enc"].free(); del runners["enc"]

0 commit comments

Comments
 (0)