Repository navigation
add_noise() before set_timesteps() uses the wrong noise level (DPMSolverMultistep, DPMSolverSinglestep, UniPC, DEIS; also SanaPipeline) #15021
Description
Describe the bug
If you call scheduler.add_noise(x0, noise, t) before set_timesteps(), these schedulers use the noise level of timestep 999 - t:
DPMSolverMultistepSchedulerDPMSolverSinglestepSchedulerUniPCMultistepSchedulerDEISMultistepScheduler
Fine-tune scripts and probe studies use this call order, and #5981 / #6006 made it a supported call order.
The cause: after __init__, self.timesteps goes from 999 down to 0, but self.sigmas (added in #6006) goes up from step 0 to step 999. add_noise finds the index of t in self.timesteps and reads self.sigmas at the same index.
With use_flow_sigmas=True (the scheduler of SanaPipeline) the result is worse. The code uses alpha_t = 1 - sigma with a VP sigma, so for t = 200 you get -24.5 * x0 + 25.5 * eps instead of 0.8 * x0 + 0.2 * eps.
After set_timesteps() everything is correct.
I found this in a probe study on SANA-1.6B. I can open a pull request with a fix and tests.
Results
SANA-1.6B with the script below, on a Kaggle T4 (16 GB), float32, with the text encoder on the CPU. a and b are the weights of x_t = a * x0 + b * eps from add_noise. The error is the relative error of the one-step clean estimate (lower is better).
| t | a, b (0.41.0) |
error, 0.41.0 | error, with my fix | error, expected flow noise |
|---|---|---|---|---|
| 200 | -24.53, 25.53 | 36.618 | 0.180 | 0.180 |
| 500 | -2.42, 3.42 | 4.615 | 0.379 | 0.379 |
| 800 | 0.28, 0.72 | 0.601 | 0.624 | 0.624 |
With my fix, add_noise gives a, b = 0.8, 0.2 / 0.5, 0.5 / 0.2, 0.8.
The default VP config has the same problem: at t = 200, DPMSolverMultistepScheduler gives a = 0.0391. DDPMScheduler gives 0.8102 at t = 200 and 0.0391 at t = 799. With a trained VP model (google/ddpm-cifar10-32), the one-step error at t = 100 is 0.590 with the four schedulers and 0.045 with DDPMScheduler.
Reproduction
The real workflow: SanaPipeline, an image through the VAE, pipe.scheduler.add_noise, then a one-step clean estimate from the transformer. The script compares this with the expected flow noise (1 - t/1000) * x0 + (t/1000) * eps. The output in "Logs" comes from python repro_sana.py --dtype float32 --text-encoder-device cpu on a 16 GB GPU (with bf16 on a larger GPU, the defaults work).
"""Reproduction: SanaPipeline scheduler add_noise() before set_timesteps().
The script encodes one image with the SANA VAE. It noises the latent with
pipe.scheduler.add_noise and does not call set_timesteps, as a fine-tune or probe script does.
Then the transformer gives a one-step clean estimate. The script repeats this with the
expected flow noise x_t = (1 - t/1000) * x0 + (t/1000) * eps.
Usage: python repro_sana.py [--model ID] [--device cuda] [--dtype bfloat16] [--image PATH]
"""
import argparse
import torch
from diffusers import SanaPipeline
from diffusers.utils import load_image
parser = argparse.ArgumentParser()
parser.add_argument("--model", default="Efficient-Large-Model/Sana_1600M_1024px_BF16_diffusers")
parser.add_argument("--device", default="cuda")
parser.add_argument("--dtype", default="bfloat16", choices=["bfloat16", "float32"])
parser.add_argument("--size", type=int, default=1024)
parser.add_argument(
"--text-encoder-device", default=None, help="Run the text encoder here, for example cpu on a 16 GB GPU."
)
parser.add_argument(
"--image",
default="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/astronaut.jpg",
)
args = parser.parse_args()
dtype = getattr(torch, args.dtype)
pipe = SanaPipeline.from_pretrained(args.model, torch_dtype=dtype)
te_device = args.text_encoder_device or args.device
pipe.text_encoder.to(te_device)
with torch.no_grad():
embeds, mask, _, _ = pipe.encode_prompt("", do_classifier_free_guidance=False, device=te_device)
embeds, mask = embeds.to(args.device), mask.to(args.device)
pipe.text_encoder = None
pipe.transformer.to(args.device)
pipe.vae.to(args.device)
image = pipe.image_processor.preprocess(load_image(args.image), height=args.size, width=args.size)
with torch.no_grad():
x0 = pipe.vae.encode(image.to(args.device, dtype)).latent * pipe.vae.config.scaling_factor
eps = torch.randn(x0.shape, generator=torch.Generator().manual_seed(0)).to(args.device, dtype)
print(f"{'t':>4} | {'a (x0)':>8} {'b (eps)':>8} | error with add_noise | error with flow noise")
for t in (200, 500, 800):
timestep = torch.tensor([t], device=args.device)
sigma = t / 1000
expected = (1 - sigma) * x0 + sigma * eps
noised = pipe.scheduler.add_noise(x0, eps, timestep)
# Least squares fit noised = a * x0 + b * eps shows the noise level that add_noise used.
design = torch.stack([x0.flatten(), eps.flatten()], 1).float()
a, b = torch.linalg.lstsq(design, noised.flatten().float()[:, None]).solution.flatten().tolist()
errors = []
for x_t in (noised, expected):
v = pipe.transformer(
x_t,
encoder_hidden_states=embeds,
encoder_attention_mask=mask,
timestep=timestep,
return_dict=False,
)[0]
x0_hat = x_t.float() - sigma * v.float()
errors.append(((x0_hat - x0.float()).norm() / x0.float().norm()).item())
print(f"{t:>4} | {a:8.3f} {b:8.3f} | {errors[0]:20.3f} | {errors[1]:21.3f}")The same problem without model weights (only the scheduler config):
import torch
from diffusers import DDPMScheduler, DPMSolverMultistepScheduler
sana = DPMSolverMultistepScheduler.from_pretrained(
"Efficient-Large-Model/Sana_1600M_1024px_BF16_diffusers", subfolder="scheduler"
)
x0, eps = torch.ones(1), torch.zeros(1)
for t in (200, 500, 800):
t = torch.tensor([t])
print("sana", t.item(), sana.add_noise(x0, eps, t).item(), sana.add_noise(eps, x0, t).item())
# The default VP config gives the noise level of 999 - t:
t = torch.tensor([200])
print("DDPM t=200", DDPMScheduler().add_noise(x0, eps, t).item())
print("DDPM t=799", DDPMScheduler().add_noise(x0, eps, torch.tensor([799])).item())
print("DPMSolver t=200", DPMSolverMultistepScheduler().add_noise(x0, eps, t).item())Logs
===== diffusers 0.41.0 (PyPI) =====
$ python repro_sana.py --dtype float32 --text-encoder-device cpu
t | a (x0) b (eps) | error with add_noise | error with flow noise
200 | -24.528 25.528 | 36.618 | 0.180
500 | -2.424 3.424 | 4.615 | 0.379
800 | 0.281 0.719 | 0.601 | 0.624
===== diffusers main (916a579) with my fix =====
$ PYTHONPATH=dsrc/src python repro_sana.py --dtype float32 --text-encoder-device cpu
t | a (x0) b (eps) | error with add_noise | error with flow noise
200 | 0.800 0.200 | 0.180 | 0.180
500 | 0.500 0.500 | 0.379 | 0.379
800 | 0.200 0.800 | 0.624 | 0.624System Info
- Diffusers version: 0.41.0 (also checked on main at 916a579, the code is the same)
- Platform: Linux-6.18.54.1-microsoft-standard-WSL2-x86_64-with-glibc2.43, and Kaggle for the GPU run
- Running on Google Colab?: No
- Python version: 3.14.4 (local), 3.13 (Kaggle)
- PyTorch version (GPU?): 2.14.1+cpu (local), 2.11.0+cu128 (Kaggle, T4)
- Huggingface_hub version: 1.33.0
- Transformers version: 5.19.0
- Accelerate version: 1.15.0
- Using GPU in script?: Yes, a Kaggle T4 (float32, text encoder on the CPU)
- Using distributed or parallel set-up in script?: No
Activity