blackboxanalytics commited on
Commit
fea5b3c
·
1 Parent(s): dd34fe0

Fix H200 fp16 instability: pin MATH SDPA, fp32 autoencoder, no TF32

Browse files

The 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.

Files changed (1) hide show
  1. engine.py +60 -10
engine.py CHANGED
@@ -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 in fp16. Called inside the @spaces.GPU
106
- window on every generation. `.to()` is a cheap no-op when the model is
107
- already placed, so we re-ensure each call rather than caching device state —
108
- that stays correct even if ZeroGPU detaches the GPU between calls. fp16 is
109
- only valid on CUDA; on CPU the model stays fp32."""
 
 
 
 
 
 
 
 
 
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 autoencoder runs in the model's dtype (fp16 on CUDA); the conditioning
264
- # audio must match it or the encoder's conv1d rejects the input dtype.
265
- model_dtype = next(model.model.parameters()).dtype
266
- lead_audio = lead_audio.to(model_dtype)
 
 
 
 
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
- with torch.no_grad():
 
 
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,