Spaces:
Running on Zero
Fix H200 fp16 instability: pin MATH SDPA, fp32 autoencoder, no TF32
Browse filesThe continuation sounds clean on local Blackwell (sm_120) but produced
sporadic loud bursts on ZeroGPU's H200 (sm_90). Root cause is fp16
numerical instability on H200, not a torch version.
- Pin F.scaled_dot_product_attention to the MATH (reference) backend
around generation. SA3's transformer calls SDPA with no backend pin and
casts q/k/v to fp16; on H200 torch 2.8+ can route that to a cuDNN/
mem-efficient kernel with known fp16 NaN/garbage bugs (the loud-burst
signature). flash-attn is absent in this image, so SDPA is the live
path and the pin engages.
- Run the autoencoder (Oobleck/DAC, Snake1d) in fp32 while the DiT stays
fp16. Snake's x + (1/(b+1e-9))*sin(a*x)^2 can exceed fp16's 65504
ceiling -> inf/NaN -> broadband noise on decode. sampling.py casts
latents to the pretransform dtype before decode, so an fp32 pretransform
upcasts the whole encode+decode path with no library change. The inpaint
lead is cast to the pretransform dtype to match.
- Disable TF32 matmul/cudnn accumulation, which compounds error through
the 8-step distilled sampler.
Verified end-to-end on Blackwell: DiT fp16 / pretransform fp32, output
finite, peak 1.0, clean burst/flatness/hf metrics, no regression.
|
@@ -37,11 +37,43 @@ Mask convention (verified against the installed library source):
|
|
| 37 |
at the front and mask [lead, lead+new], so SA3 keeps the lead and generates a
|
| 38 |
fresh `new`-second tail that continues from the clip's end.
|
| 39 |
"""
|
|
|
|
| 40 |
import time
|
| 41 |
|
| 42 |
import numpy as np
|
| 43 |
import torch
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
MODEL_ID = "stabilityai/stable-audio-3-small-music"
|
| 46 |
|
| 47 |
SR = 44100 # SA3 native sample rate (model_config: sample_rate)
|
|
@@ -102,16 +134,28 @@ def preload():
|
|
| 102 |
|
| 103 |
|
| 104 |
def _ensure_on_device():
|
| 105 |
-
"""Ensure weights are on the GPU
|
| 106 |
-
window on every generation. `.to()` is a cheap no-op
|
| 107 |
-
already placed, so we re-ensure each call rather than caching device
|
| 108 |
-
that stays correct even if ZeroGPU detaches the GPU between calls.
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
global _model, _on_device
|
| 111 |
dev = _device()
|
| 112 |
_model = _model.to(dev)
|
| 113 |
if dev == "cuda":
|
| 114 |
_model = _model.to(torch.float16)
|
|
|
|
|
|
|
|
|
|
| 115 |
_on_device = dev
|
| 116 |
return _model
|
| 117 |
|
|
@@ -260,10 +304,14 @@ def continue_audio(clip_path, total_seconds, prompt="", cfg_scale=DEFAULT_CFG,
|
|
| 260 |
# a long source no longer fills the buffer and starves the masked region.
|
| 261 |
lead_samples = min(int(round(lead * SR)), source.shape[-1])
|
| 262 |
lead_audio = source[:, -lead_samples:]
|
| 263 |
-
# the
|
| 264 |
-
#
|
| 265 |
-
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
mask_start, mask_end = lead, buffer_seconds
|
| 269 |
prompt = (prompt or "").strip()
|
|
@@ -275,7 +323,9 @@ def continue_audio(clip_path, total_seconds, prompt="", cfg_scale=DEFAULT_CFG,
|
|
| 275 |
|
| 276 |
def _draw(draw_seed):
|
| 277 |
"""One full SA3 inpaint draw -> normalized generated tail (2, M)."""
|
| 278 |
-
|
|
|
|
|
|
|
| 279 |
output = generate_diffusion_cond_inpaint(
|
| 280 |
model,
|
| 281 |
steps=STEPS,
|
|
|
|
| 37 |
at the front and mask [lead, lead+new], so SA3 keeps the lead and generates a
|
| 38 |
fresh `new`-second tail that continues from the clip's end.
|
| 39 |
"""
|
| 40 |
+
import contextlib
|
| 41 |
import time
|
| 42 |
|
| 43 |
import numpy as np
|
| 44 |
import torch
|
| 45 |
|
| 46 |
+
# --- H200 (sm_90) fp16 numerical-stability hardening -------------------------
|
| 47 |
+
# CODA sounds perfect on local Blackwell (sm_120) but produced "sporadic loud
|
| 48 |
+
# bursts" on ZeroGPU's H200. Root cause is fp16 numerical instability on H200,
|
| 49 |
+
# not a torch version: (1) torch 2.8 changed cuDNN SDPA backend selection for
|
| 50 |
+
# H200 and the cuDNN/mem-efficient fp16 SDPA kernels have known NaN/garbage bugs
|
| 51 |
+
# (pytorch#139298/#166211/#124877/#112577) — exactly the "loud burst" signature;
|
| 52 |
+
# (2) TF32 matmul accumulation adds error that compounds through the 8-step
|
| 53 |
+
# distilled sampler. We pin the math (reference) SDPA backend around generation
|
| 54 |
+
# and force full-precision matmul accumulation. Both are no-ops on CPU.
|
| 55 |
+
torch.backends.cuda.matmul.allow_tf32 = False
|
| 56 |
+
torch.backends.cudnn.allow_tf32 = False
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
from torch.nn.attention import SDPBackend, sdpa_kernel as _sdpa_kernel
|
| 60 |
+
except Exception: # very old/unexpected torch
|
| 61 |
+
SDPBackend = None
|
| 62 |
+
_sdpa_kernel = None
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _sdpa_math():
|
| 66 |
+
"""Context manager that forces F.scaled_dot_product_attention onto the MATH
|
| 67 |
+
(reference) backend for the wrapped call. SA3's transformer calls SDPA with
|
| 68 |
+
no backend pin and casts q/k/v to fp16; on H200 torch may route that to a
|
| 69 |
+
cuDNN/mem-efficient kernel with the known fp16 NaN bug. MATH is the stable
|
| 70 |
+
reference path. Returns a fresh context each call; a no-op if the API is
|
| 71 |
+
missing (older torch) or on CPU."""
|
| 72 |
+
if _sdpa_kernel is None:
|
| 73 |
+
return contextlib.nullcontext()
|
| 74 |
+
return _sdpa_kernel(SDPBackend.MATH)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
MODEL_ID = "stabilityai/stable-audio-3-small-music"
|
| 78 |
|
| 79 |
SR = 44100 # SA3 native sample rate (model_config: sample_rate)
|
|
|
|
| 134 |
|
| 135 |
|
| 136 |
def _ensure_on_device():
|
| 137 |
+
"""Ensure weights are on the GPU with the right per-component dtype. Called
|
| 138 |
+
inside the @spaces.GPU window on every generation. `.to()` is a cheap no-op
|
| 139 |
+
when already placed, so we re-ensure each call rather than caching device
|
| 140 |
+
state — that stays correct even if ZeroGPU detaches the GPU between calls.
|
| 141 |
+
|
| 142 |
+
Dtype split (the H200 quality fix): the DiT runs in fp16 (fast, and Stability
|
| 143 |
+
loads the model fp16), but the autoencoder/pretransform runs in fp32. SA3's
|
| 144 |
+
Oobleck/DAC decoder uses Snake1d activations — x + (1/(beta+1e-9))*sin(a*x)^2
|
| 145 |
+
— whose reciprocal*sin^2 term can exceed fp16's ~65504 ceiling, giving
|
| 146 |
+
inf/NaN -> loud broadband noise on decode. fp32 there has ample headroom.
|
| 147 |
+
sampling.py casts the sampled latents to the pretransform's dtype right
|
| 148 |
+
before `decode`, so an fp32 pretransform makes the WHOLE encode+decode path
|
| 149 |
+
fp32 with no library change; only the DiT stays fp16. All fp16/fp32 here is
|
| 150 |
+
CUDA-only; on CPU the model stays fp32 throughout."""
|
| 151 |
global _model, _on_device
|
| 152 |
dev = _device()
|
| 153 |
_model = _model.to(dev)
|
| 154 |
if dev == "cuda":
|
| 155 |
_model = _model.to(torch.float16)
|
| 156 |
+
# keep the autoencoder in fp32 (Snake1d fp16 overflow guard)
|
| 157 |
+
if getattr(_model, "pretransform", None) is not None:
|
| 158 |
+
_model.pretransform.to(torch.float32)
|
| 159 |
_on_device = dev
|
| 160 |
return _model
|
| 161 |
|
|
|
|
| 304 |
# a long source no longer fills the buffer and starves the masked region.
|
| 305 |
lead_samples = min(int(round(lead * SR)), source.shape[-1])
|
| 306 |
lead_audio = source[:, -lead_samples:]
|
| 307 |
+
# the lead is encoded by the autoencoder (pretransform), which we now run in
|
| 308 |
+
# fp32; its conv1d rejects a mismatched input dtype, so match the
|
| 309 |
+
# pretransform's parameter dtype (fp32 on CUDA), NOT the DiT's fp16. Resample
|
| 310 |
+
# is a no-op here (clip is already 44.1k), so prepare_audio keeps this dtype.
|
| 311 |
+
ae = getattr(model, "pretransform", None)
|
| 312 |
+
ae_dtype = (next(ae.parameters()).dtype if ae is not None
|
| 313 |
+
else next(model.model.parameters()).dtype)
|
| 314 |
+
lead_audio = lead_audio.to(ae_dtype)
|
| 315 |
|
| 316 |
mask_start, mask_end = lead, buffer_seconds
|
| 317 |
prompt = (prompt or "").strip()
|
|
|
|
| 323 |
|
| 324 |
def _draw(draw_seed):
|
| 325 |
"""One full SA3 inpaint draw -> normalized generated tail (2, M)."""
|
| 326 |
+
# _sdpa_math() forces the reference attention backend for the whole
|
| 327 |
+
# sample loop — the H200 fp16-SDPA-garbage guard (no-op on CPU).
|
| 328 |
+
with torch.no_grad(), _sdpa_math():
|
| 329 |
output = generate_diffusion_cond_inpaint(
|
| 330 |
model,
|
| 331 |
steps=STEPS,
|