Skip to content

Commit eb8a2fd

Browse files
Add opt-in FP8 DiT TensorRT engine for SA3-medium
Producer recipe (build_dit_fp8.py) builds a ModelOpt FP8 GEMM-trunk DiT on top of the FP16-mixed graph: FP8 PTQ on MatMul/Gemm, initializer repair plus activation-scale recalibration, re-applied FP32 islands (RMSNorm/Softmax/RoPE plus the conditioning front-end), and per-channel weight scales. make_calib.py captures calibration inputs from the model's own pingpong generate(), pulling prompts from interface/reprompt.py. ~1.8x faster steps than FP16-mixed at B=1, amortizing further under batched dispatch. Consumer wiring adds sa3-m-fp8 as an opt-in target (excluded from all/all-both and 'build all missing'; built only by explicit name, gated on the published ONNX) and a --precision fp8 selection that pairs the FP8 DiT with the FP16-mixed decoder, guarded so non-medium DiTs cannot request it.
1 parent 9b6be18 commit eb8a2fd

8 files changed

Lines changed: 1231 additions & 12 deletions

File tree

optimized/tensorRT/.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ onnx/
1212
*.onnx
1313
*.onnx.data
1414

15+
# FP8 calibration data — captured by build/make_calib.py from the model
16+
# checkpoint, a reproducible producer artifact (~hundreds of MB). Regenerated
17+
# on demand, never committed.
18+
*.calib.npz
19+
1520
# T5 tokenizer — generally ignored (downloaded under models/<arch>/t5gemma/
1621
# alongside the engine for legacy fallback), but the canonical copy ships
1722
# bundled at scripts/tokenizer.json (arch-agnostic, 34 MB) so the build path
@@ -36,3 +41,7 @@ __pycache__/
3641
# But keep __pycache__/ ignored even under build/ (the un-ignore above would
3742
# accidentally re-include build/__pycache__/ otherwise).
3843
build/**/__pycache__/
44+
# Same for make_calib.py's captured calibration npz (the *.calib.npz rule
45+
# above is otherwise shadowed by the build/ un-ignore — make_calib writes it
46+
# next to the build scripts by default).
47+
build/**/*.calib.npz

optimized/tensorRT/build/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ python build_from_onnx.py same-s-decoder-fp32 # canonical ONNX is already FP32
6969
python build_from_onnx.py sa3-m-fp32 # reads HF dit.onnx (already FP32)
7070
python build_from_onnx.py all-fp32 # every FP32 target
7171
python build_from_onnx.py all-both # canonical + FP32
72+
73+
# FP8 variant — opt-in, DiT-only. ~1.8x faster steps than FP16-mixed.
74+
# Pair with `sa3_trt --precision fp8` at inference (fp8 DiT + fp16mixed decoder).
75+
python build_from_onnx.py sa3-m-fp8 # reads HF dit_fp8.onnx (ModelOpt PTQ)
7276
```
7377

7478
### Consumer deps
@@ -148,6 +152,33 @@ This wraps every RMSNorm chain, attention `Softmax`, and the RoPE region in `Cas
148152

149153
Naive `BuilderFlag.FP16` (without the surgery) catastrophically overflows in RMSNorm variance + attention softmax — the islands are mandatory. BF16 was tried earlier and compounds quantisation error over 8 sampling steps (cos-sim drifts from 0.99 single-step to 0.81 final-latent vs PT FP32) — audibly degraded.
150154

155+
### FP8 DiT (opt-in, ~1.8x)
156+
157+
`build_dit_fp8.py` extends the FP16-mixed recipe with a ModelOpt FP8 GEMM trunk: it takes the `dit_fp16mixed.onnx` plus a calibration `.npz` (real DiT inputs across the pingpong schedule) and produces `dit_fp8.onnx`: fp8 weight/activation Q/DQ on the MatMuls, the same FP32 islands re-applied (plus the conditioning front-end, which must stay FP32 or the t>=0.984 timestep features flush), and per-channel weight scales. Validated on sa3-m vs the FP16-mixed engine: worst single-step velocity cosine 0.978 (latent cosine 0.998), 8-step compounded final-latent cosine ~0.96 to 0.976 (prompt-dependent), ~11.2 ms/step (vs ~19.9), ~1.8x. Under the stochastic pingpong sampler it yields a different but comparable sample.
158+
159+
First capture the calibration data from the model checkpoint with `make_calib.py` (drives the model's own pingpong `generate()` to record real DiT inputs across the sampling schedule; prompts come from the repo's own `interface/reprompt.py` Music examples, the deployment-matched reprompt format):
160+
161+
```bash
162+
python make_calib.py \
163+
--model-config <MODELS_ROOT>/SA3-M-hf/model_config.json \
164+
--checkpoint <MODELS_ROOT>/SA3-M-hf/model.safetensors \
165+
--out sa3-m.calib.npz
166+
```
167+
168+
Then build the engine:
169+
170+
```bash
171+
python build_dit_fp8.py \
172+
--input $HF/onnx/sa3-m/dit_fp16mixed.onnx \
173+
--calib sa3-m.calib.npz \
174+
--onnx $HF/onnx/sa3-m/dit_fp8.onnx \
175+
--engine ../models/$ARCH/sa3-m/dit_fp8.trt
176+
```
177+
178+
`make_calib.py` needs only the repo + checkpoint (`torch`, `numpy`, `stable_audio_3`). `build_dit_fp8.py` additionally requires `nvidia-modelopt` + `onnxruntime-gpu` (the calibration-repair pass); consumers compile the published `dit_fp8.onnx` with plain `build_from_onnx.py sa3-m-fp8` (STRONGLY_TYPED, no ModelOpt, no calibration).
179+
180+
> **Not yet on HF.** `dit_fp8.onnx` + `dit_fp8.onnx.data` are not in the model repo yet, so `build_from_onnx.py sa3-m-fp8` and `sa3_trt --precision fp8` 404 until a producer run uploads them (under exactly those filenames). The consumer recipe and `--precision fp8` plumbing land here so the wiring is reviewed; the artifact upload is the follow-up step.
181+
151182
Each script also writes the ONNX to `<HF_REPO>/onnx/<engine>/<file>.onnx`. After all 8 are done:
152183

153184
```bash
@@ -166,6 +197,8 @@ git push
166197
| `build_from_onnx.py` | One target → download ONNX from HF + compile to TRT. **For the SA3 DiTs, pulls `dit_fp16mixed.onnx` (the pre-processed island-wrapped graph)** so the consumer just needs to invoke `STRONGLY_TYPED` compilation — no `onnx-graphsurgeon` required | consumer |
167198
| `build_dit_profile.py` | Build a DiT with custom `(min, opt, max)` profile shapes (experimental — short-form / fixed-shape variants). Operates on either ONNX flavor. | consumer |
168199
| `build_dit_fp16mixed.py` | **Producer-side** ONNX surgery: takes the canonical FP32 `dit.onnx`, finds RMSNorm chains + attention `Softmax` + RoPE region, wraps each in `Cast(FP32) ↔ Cast(FP16)` islands, converts non-island weights to FP16, and writes both the modified `dit_fp16mixed.onnx` AND the TRT engine. Only re-run when the model retrains or the island recipe changes. Requires `onnx` + `onnx-graphsurgeon`. | producer |
200+
| `make_calib.py` | **Producer-side** FP8 calibration capture: drives the model's own pingpong `generate()` and records the six DiT engine inputs across the schedule into a `*.calib.npz` for `build_dit_fp8.py`. Needs only the checkpoint (`torch` + `stable_audio_3`). | producer |
201+
| `build_dit_fp8.py` | **Producer-side** FP8 trunk on top of `dit_fp16mixed.onnx`: ModelOpt FP8 PTQ (MatMul/Gemm, max calibration from a `.npz`), restores ModelOpt-corrupted initializers + recalibrates activation scales, re-applies the FP32 islands (incl. the conditioning front-end), and per-channel weight scales. Writes `dit_fp8.onnx` + the TRT engine. ~1.8x faster steps than FP16-mixed. Requires `nvidia-modelopt` + `onnxruntime-gpu`. | producer |
169202
| `build_t5gemma.py` | Trace + export T5Gemma encoder ONNX + build TRT | producer |
170203
| `build_same_s_decoder.py` | Trace + export SAME-S decoder ONNX + build TRT | producer |
171204
| `build_same_s_encoder.py` | Trace + export SAME-S encoder ONNX + build TRT | producer |

optimized/tensorRT/build/build.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ def _from_onnx(name):
9393
{"label": "[opt-in] DiT sm-sfx FP32",
9494
"command": _from_onnx("sa3-sm-sfx-fp32"),
9595
"outputs": ["sa3-sm-sfx/dit_fp32.trt"]},
96+
# FP8 variant, opt-in, DiT-only. ~1.8x faster steps than FP16-mixed
97+
# (ModelOpt PTQ; producer build_dit_fp8.py). A different but comparable
98+
# sample under the stochastic pingpong sampler.
99+
{"label": "[opt-in] DiT medium FP8 (~1.8x)",
100+
"command": _from_onnx("sa3-m-fp8"),
101+
"outputs": ["sa3-m/dit_fp8.trt"],
102+
"opt_in": True}, # built only by number, never via "Build all missing"
96103
]
97104

98105

@@ -134,12 +141,19 @@ def render_menu(arch: str, arch_dir: Path) -> list[bool]:
134141
size_s = fmt_size(sz) if sz >= 0 else f"{DIM}(missing){RESET}"
135142
print(f" {tick} {DIM}{rel}{RESET} {size_s}")
136143

137-
n_missing = built_flags.count(False)
144+
# "Build all missing" covers default targets only; opt-in variants (FP8)
145+
# are built by number, so they are counted and reported separately.
146+
n_missing = sum(1 for t, ok in zip(TARGETS, built_flags)
147+
if not ok and not t.get("opt_in"))
148+
n_optin_missing = sum(1 for t, ok in zip(TARGETS, built_flags)
149+
if not ok and t.get("opt_in"))
138150
print()
139151
if n_missing == 0:
140-
print(f" {BOLD}{GREEN}[A]{RESET} Build all missing {DIM}(nothing missing — all engines built){RESET}")
152+
print(f" {BOLD}{GREEN}[A]{RESET} Build all missing {DIM}(nothing missing — all default engines built){RESET}")
141153
else:
142154
print(f" {BOLD}{YELLOW}[A]{RESET} Build all missing {DIM}({n_missing} target(s)){RESET}")
155+
if n_optin_missing:
156+
print(f" {DIM}(+{n_optin_missing} opt-in target(s) not in [A] — build by number){RESET}")
143157
print(f" {BOLD}{DIM}[Q]{RESET} Quit")
144158
return built_flags
145159

@@ -180,7 +194,9 @@ def main() -> int:
180194
return 0
181195

182196
if choice in ("a", "all"):
183-
missing = [t for t, ok in zip(TARGETS, built_flags) if not ok]
197+
# Opt-in variants (FP8) are excluded; build them by number.
198+
missing = [t for t, ok in zip(TARGETS, built_flags)
199+
if not ok and not t.get("opt_in")]
184200
if not missing:
185201
print(f" {DIM}Nothing to build.{RESET}")
186202
continue

0 commit comments

Comments
 (0)