chore: import upstream snapshot with attribution
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:22:06 +08:00
commit cddb07a176
3370 changed files with 685519 additions and 0 deletions
@@ -0,0 +1,601 @@
"""ER-SDE (Extended Reverse-time SDE) ``diffusers`` scheduler.
Implements the multistep Taylor-expansion solver from:
Cui, Q., Zhang, X., Lu, Z., & Liao, Q. (2023).
Elucidating the solution space of extended reverse-time SDE
for diffusion models. arXiv:2309.06169.
https://arxiv.org/abs/2309.06169
Reference implementation (MIT-licensed):
https://github.com/QinpengCui/ER-SDE-Solver/blob/main/er_sde_solver.py
This scheduler unifies two regimes under a single API:
* **VP-SDE** (``use_flow_sigmas=False``) — Stable Diffusion / SDXL style models
with epsilon, x0, or v prediction. Uses the standard
``alpha_t = 1 / sqrt(1 + sigma^2), sigma_t = sigma * alpha_t`` parameterization
and ports ``vp_*_order_*`` from the reference impl.
* **Rectified flow / flow matching** (``use_flow_sigmas=True``) — FLUX, Z-Image,
Anima style models with flow_prediction. Uses ``alpha_t = 1 - sigma, sigma_t = sigma``
and the rectified-flow integral helpers defined locally (``_fn``,
``_integral_one_over_fn``, ``_integral_lam_minus_curr_over_fn``).
The rectified-flow integral helpers are kept local so this class is self-contained.
"""
from __future__ import annotations
import math
from typing import List, Optional, Tuple, Union
import numpy as np
import torch
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.schedulers.scheduling_utils import KarrasDiffusionSchedulers, SchedulerMixin, SchedulerOutput
from diffusers.utils.torch_utils import randn_tensor
# Number of sample points for the left Riemann sums approximating the
# Taylor-extension integrals. Matches the reference impl's nums_intergrate=100.
_INTEGRAL_NUM_POINTS = 100
def _fn(x: float) -> float:
"""ER-SDE noise-scale function ``SDE_5`` (paper appendix A.8).
Mirrors ``customized_func(..., func_type=7)`` in the reference impl —
the variant the paper recommends and tests for fast (~20 NFE) sampling.
"""
return x * (math.exp(x**0.3) + 10.0)
def _integral_one_over_fn(lambda_next: float, lambda_curr: float) -> float:
"""Left Riemann sum of int_{lambda_next}^{lambda_curr} 1/_fn(lam) dlam.
Precondition: ``lambda_next > 0``. The integrand has a logarithmic singularity
at ``lam = 0`` (``_fn(0) = 0``); callers must skip this when ``sigma_next == 0``.
"""
delta = lambda_curr - lambda_next
if delta <= 0:
return 0.0
step = delta / _INTEGRAL_NUM_POINTS
total = 0.0
for k in range(_INTEGRAL_NUM_POINTS):
lam = lambda_next + k * step
total += step / _fn(lam)
return total
def _integral_lam_minus_curr_over_fn(lambda_next: float, lambda_curr: float) -> float:
"""Left Riemann sum of int_{lambda_next}^{lambda_curr} (lam - lambda_curr)/_fn(lam) dlam.
Precondition: ``lambda_next > 0``. Same singularity at ``lam = 0`` as
:func:`_integral_one_over_fn`.
"""
delta = lambda_curr - lambda_next
if delta <= 0:
return 0.0
step = delta / _INTEGRAL_NUM_POINTS
total = 0.0
for k in range(_INTEGRAL_NUM_POINTS):
lam = lambda_next + k * step
total += step * (lam - lambda_curr) / _fn(lam)
return total
class ERSDEScheduler(SchedulerMixin, ConfigMixin):
"""``diffusers`` scheduler for the ER-SDE multistep solver.
See module docstring for paper / reference-impl citations.
Args:
num_train_timesteps: Number of diffusion steps used during training.
beta_start: VP-SDE beta schedule start (ignored when ``use_flow_sigmas=True``).
beta_end: VP-SDE beta schedule end (ignored when ``use_flow_sigmas=True``).
beta_schedule: ``"linear"``, ``"scaled_linear"``, or ``"squaredcos_cap_v2"``.
trained_betas: Override betas with a pre-computed schedule.
prediction_type: ``"epsilon"``, ``"v_prediction"``, or ``"flow_prediction"``.
solver_order: Multistep order (1, 2, or 3). The solver auto-warms from order 1.
use_flow_sigmas: If True, use the rectified-flow parameterization
(``alpha_t = 1 - sigma``); else VP-SDE.
flow_shift: Sigma shift applied to the default flow schedule.
stochastic: If True, inject noise (full ER-SDE). If False, deterministic
ODE companion — same Taylor expansion with the noise term zeroed.
sigma_one_tolerance: Boundary tolerance for the ``sigma = 1`` limit
(rectified-flow only). Numerically paranoid; keep small.
timestep_spacing: ``"linspace"``, ``"leading"``, or ``"trailing"``.
steps_offset: Offset added to ``"leading"`` timesteps.
"""
_compatibles = [e.name for e in KarrasDiffusionSchedulers]
order = 1
@register_to_config
def __init__(
self,
num_train_timesteps: int = 1000,
beta_start: float = 0.00085,
beta_end: float = 0.012,
beta_schedule: str = "scaled_linear",
trained_betas: Optional[Union[np.ndarray, List[float]]] = None,
prediction_type: str = "epsilon",
solver_order: int = 3,
use_flow_sigmas: bool = False,
flow_shift: float = 1.0,
stochastic: bool = True,
sigma_one_tolerance: float = 1e-6,
timestep_spacing: str = "linspace",
steps_offset: int = 0,
):
if prediction_type not in ("epsilon", "v_prediction", "flow_prediction"):
raise ValueError(
f"prediction_type must be one of 'epsilon', 'v_prediction', 'flow_prediction', got {prediction_type!r}"
)
if solver_order not in (1, 2, 3):
raise ValueError(f"solver_order must be 1, 2, or 3, got {solver_order}")
if prediction_type == "flow_prediction" and not use_flow_sigmas:
# Not strictly invalid, but almost certainly a misconfiguration.
raise ValueError("prediction_type='flow_prediction' requires use_flow_sigmas=True (rectified-flow regime).")
# VP-SDE noise schedule (only used when use_flow_sigmas=False).
if trained_betas is not None:
self.betas = torch.tensor(trained_betas, dtype=torch.float32)
elif beta_schedule == "linear":
self.betas = torch.linspace(beta_start, beta_end, num_train_timesteps, dtype=torch.float32)
elif beta_schedule == "scaled_linear":
self.betas = torch.linspace(beta_start**0.5, beta_end**0.5, num_train_timesteps, dtype=torch.float32) ** 2
elif beta_schedule == "squaredcos_cap_v2":
# Glide cosine schedule.
betas = []
for i in range(num_train_timesteps):
t1 = i / num_train_timesteps
t2 = (i + 1) / num_train_timesteps
a1 = math.cos((t1 + 0.008) / 1.008 * math.pi / 2) ** 2
a2 = math.cos((t2 + 0.008) / 1.008 * math.pi / 2) ** 2
betas.append(min(1 - a2 / a1, 0.999))
self.betas = torch.tensor(betas, dtype=torch.float32)
else:
raise NotImplementedError(f"beta_schedule {beta_schedule!r} is not implemented for ERSDEScheduler")
self.alphas = 1.0 - self.betas
self.alphas_cumprod = torch.cumprod(self.alphas, dim=0)
# Default sigmas (VP-SDE form). Overwritten in set_timesteps.
self.sigmas = ((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5
# Standard deviation of initial noise distribution (per Euler convention).
self.init_noise_sigma = 1.0
self.num_inference_steps: Optional[int] = None
timesteps = np.linspace(0, num_train_timesteps - 1, num_train_timesteps, dtype=np.float32)[::-1].copy()
self.timesteps = torch.from_numpy(timesteps)
# Multistep history. ``model_outputs`` stores x0 predictions; ``_sigma_history``
# stores the sigma at which each prediction was made. Both are FIFO with
# length == solver_order. Slot ``-1`` is the most recent.
self.model_outputs: List[Optional[torch.Tensor]] = [None] * solver_order
self._sigma_history: List[Optional[float]] = [None] * solver_order
self.lower_order_nums = 0
self._step_index: Optional[int] = None
self._begin_index: Optional[int] = None
self.sigmas = self.sigmas.to("cpu")
# ---- Index plumbing (mirrors DPM++) ---------------------------------------
@property
def step_index(self) -> Optional[int]:
return self._step_index
@property
def begin_index(self) -> Optional[int]:
return self._begin_index
def set_begin_index(self, begin_index: int = 0) -> None:
self._begin_index = begin_index
def index_for_timestep(
self,
timestep: Union[int, torch.Tensor],
schedule_timesteps: Optional[torch.Tensor] = None,
) -> int:
if schedule_timesteps is None:
schedule_timesteps = self.timesteps
index_candidates = (schedule_timesteps == timestep).nonzero()
if len(index_candidates) == 0:
return len(self.timesteps) - 1
# On the very first step, prefer the second match if duplicated, so
# img2img doesn't accidentally skip a sigma.
if len(index_candidates) > 1:
return index_candidates[1].item()
return index_candidates[0].item()
def _init_step_index(self, timestep: Union[int, torch.Tensor]) -> None:
if self.begin_index is None:
if isinstance(timestep, torch.Tensor):
timestep = timestep.to(self.timesteps.device)
self._step_index = self.index_for_timestep(timestep)
else:
self._step_index = self._begin_index
# ---- Timestep / sigma scheduling ------------------------------------------
def set_timesteps(
self,
num_inference_steps: Optional[int] = None,
device: Optional[Union[str, torch.device]] = None,
sigmas: Optional[Union[List[float], np.ndarray, torch.Tensor]] = None,
timesteps: Optional[List[int]] = None,
) -> None:
"""Set the discrete timesteps used for inference.
Exactly one of ``num_inference_steps``, ``timesteps``, or ``sigmas`` must
be provided. The ``sigmas`` form (mirroring :class:`EulerDiscreteScheduler`)
lets Anima/FLUX/Z-Image inject pre-shifted sigma schedules directly.
"""
n_set = sum(x is not None for x in (num_inference_steps, timesteps, sigmas))
if n_set != 1:
raise ValueError("Must pass exactly one of `num_inference_steps`, `timesteps`, or `sigmas`.")
if sigmas is not None:
if isinstance(sigmas, torch.Tensor):
sigmas_np = sigmas.detach().cpu().numpy().astype(np.float32)
else:
sigmas_np = np.array(sigmas, dtype=np.float32)
num_inference_steps = len(sigmas_np) - 1
# Timesteps in the rectified-flow / Anima convention scale sigma to t.
# For VP-SDE this approximation is wrong but timesteps are only used
# for indexing; the algebra runs entirely off self.sigmas.
timesteps_np = (sigmas_np[:-1] * self.config.num_train_timesteps).astype(np.float32)
elif timesteps is not None:
timesteps_np = np.array(timesteps, dtype=np.float32)
num_inference_steps = len(timesteps_np)
sigmas_np = self._sigmas_for_timesteps(timesteps_np)
else:
assert num_inference_steps is not None
timesteps_np = self._default_timesteps(num_inference_steps)
sigmas_np = self._sigmas_for_timesteps(timesteps_np)
self.num_inference_steps = num_inference_steps
self.sigmas = torch.from_numpy(sigmas_np.astype(np.float32))
self.timesteps = torch.from_numpy(timesteps_np.astype(np.float32)).to(device=device)
# Reset multistep state.
self.model_outputs = [None] * self.config.solver_order
self._sigma_history = [None] * self.config.solver_order
self.lower_order_nums = 0
self._step_index = None
self._begin_index = None
self.sigmas = self.sigmas.to("cpu")
def _default_timesteps(self, num_inference_steps: int) -> np.ndarray:
"""Standard linspace/leading/trailing schedule (VP-SDE timesteps)."""
if self.config.timestep_spacing == "linspace":
timesteps = (
np.linspace(0, self.config.num_train_timesteps - 1, num_inference_steps + 1)
.round()[::-1][:-1]
.copy()
.astype(np.float32)
)
elif self.config.timestep_spacing == "leading":
step_ratio = self.config.num_train_timesteps // (num_inference_steps + 1)
timesteps = (
(np.arange(0, num_inference_steps + 1) * step_ratio).round()[::-1][:-1].copy().astype(np.float32)
)
timesteps += self.config.steps_offset
elif self.config.timestep_spacing == "trailing":
step_ratio = self.config.num_train_timesteps / num_inference_steps
timesteps = np.arange(self.config.num_train_timesteps, 0, -step_ratio).round().copy().astype(np.float32)
timesteps -= 1
else:
raise ValueError(
f"timestep_spacing {self.config.timestep_spacing!r} must be one of 'linspace', 'leading', 'trailing'"
)
return timesteps
def _sigmas_for_timesteps(self, timesteps_np: np.ndarray) -> np.ndarray:
"""Build the sigma schedule (with terminal 0 appended) for given timesteps."""
if self.config.use_flow_sigmas:
# Rectified-flow sigmas in [0, 1], time-shifted per Anima/FLUX convention.
num_inference_steps = len(timesteps_np)
alphas = np.linspace(1, 1 / self.config.num_train_timesteps, num_inference_steps + 1)
sigmas = 1.0 - alphas
shift = self.config.flow_shift
sigmas = np.flip(shift * sigmas / (1 + (shift - 1) * sigmas))[:-1].copy()
# Terminal sigma is exactly 0.
return np.concatenate([sigmas, [0.0]]).astype(np.float32)
# VP-SDE: interpolate against the train sigmas using timestep indexing.
train_sigmas = np.array(((1 - self.alphas_cumprod) / self.alphas_cumprod) ** 0.5)
sigmas = np.interp(timesteps_np, np.arange(0, len(train_sigmas)), train_sigmas)
return np.concatenate([sigmas, [0.0]]).astype(np.float32)
# ---- Math helpers ---------------------------------------------------------
def _sigma_to_alpha_sigma_t(self, sigma: float) -> Tuple[float, float]:
"""Map ``sigma`` to ``(alpha_t, sigma_t)``.
Rectified flow: ``alpha_t = 1 - sigma, sigma_t = sigma``.
VP-SDE: ``alpha_t = 1 / sqrt(1 + sigma^2), sigma_t = sigma * alpha_t``.
"""
if self.config.use_flow_sigmas:
return 1.0 - sigma, sigma
alpha_t = 1.0 / math.sqrt(1.0 + sigma * sigma)
return alpha_t, sigma * alpha_t
@staticmethod
def _lambda(alpha_t: float, sigma_t: float) -> float:
"""ER-SDE ``lambda = sigma_t / alpha_t`` — the noise-to-signal ratio.
This matches the reference impl's ``lambdas = sigmas / alphas`` in both
VP and rectified-flow regimes (see ``vp_*_order_*`` in
``https://github.com/QinpengCui/ER-SDE-Solver``). For VP-SDE this equals
the stored sigma; for rectified flow it equals ``sigma / (1 - sigma)``.
Diverges at ``sigma_t = alpha_t = 0`` (rectified flow at sigma=1) — the
boundary branch in :meth:`_first_order_update` handles that case.
"""
if alpha_t == 0.0:
return float("inf")
return sigma_t / alpha_t
# ---- Model output conversion ----------------------------------------------
def _convert_model_output(self, model_output: torch.Tensor, sample: torch.Tensor) -> torch.Tensor:
"""Convert raw model output to an ``x0`` prediction at the current sigma."""
sigma = float(self.sigmas[self.step_index].item())
if self.config.prediction_type == "flow_prediction":
# v = (x - x0) / sigma => x0 = x - sigma * v
return sample - sigma * model_output
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
if self.config.prediction_type == "epsilon":
return (sample - sigma_t * model_output) / alpha_t
if self.config.prediction_type == "v_prediction":
return alpha_t * sample - sigma_t * model_output
raise ValueError(f"Unsupported prediction_type {self.config.prediction_type!r}")
# ---- Order-N updates -------------------------------------------------------
def _first_order_update(
self,
x0: torch.Tensor,
sample: torch.Tensor,
sigma_curr: float,
sigma_next: float,
noise: Optional[torch.Tensor],
) -> torch.Tensor:
"""Order-1 ER-SDE step (ports ``vp_1_order`` / ``er_sde_rf_step`` order-1 branch)."""
# Rectified-flow boundary: sigma_curr ~= 1 means alpha_curr ~= 0 so lambda diverges.
# Closed-form limit (er_sde.py:136-142): x_next = (1 - sigma_next) * x0 + sigma_next * noise.
if self.config.use_flow_sigmas and 1.0 - sigma_curr < self.config.sigma_one_tolerance:
x_next = (1.0 - sigma_next) * x0
if self.config.stochastic and noise is not None and sigma_next > 0.0:
x_next = x_next + sigma_next * noise
return x_next
alpha_curr, sigma_curr_t = self._sigma_to_alpha_sigma_t(sigma_curr)
alpha_next, sigma_next_t = self._sigma_to_alpha_sigma_t(sigma_next)
# Reference impl uses lambda = sigma_t / alpha_t in both VP and flow regimes.
lambda_curr = self._lambda(alpha_curr, sigma_curr_t)
# At the terminal step, sigma_next == 0 so lambda_next == 0 and fn_next == 0.
lambda_next = self._lambda(alpha_next, sigma_next_t) if sigma_next_t > 0.0 else 0.0
fn_curr = _fn(lambda_curr)
fn_next = _fn(lambda_next)
r_fn = fn_next / fn_curr if fn_curr != 0.0 else 0.0
r_alphas = alpha_next / alpha_curr
# Stochastic noise std (paper appendix eq. for ER-SDE_5 variance).
# ``inner`` can underflow to tiny negatives by roundoff; clip.
inner = lambda_next**2 - lambda_curr**2 * r_fn**2
if inner < 0.0:
inner = 0.0
noise_std = math.sqrt(inner) * alpha_next
x_next = r_alphas * r_fn * sample + alpha_next * (1.0 - r_fn) * x0
if self.config.stochastic and noise is not None and sigma_next > 0.0:
x_next = x_next + noise_std * noise
return x_next
def _second_order_update(
self,
sample: torch.Tensor,
sigma_curr: float,
sigma_next: float,
noise: Optional[torch.Tensor],
) -> torch.Tensor:
"""Order-2 ER-SDE step (ports ``vp_2_order_taylor``)."""
x0 = self.model_outputs[-1]
old_x0 = self.model_outputs[-2]
sigma_prev_curr = self._sigma_history[-2]
assert x0 is not None and old_x0 is not None and sigma_prev_curr is not None
# If the previous step used the sigma=1 closed-form limit, the finite-difference
# derivative across that boundary is meaningless — fall back to order 1.
if self.config.use_flow_sigmas and 1.0 - sigma_prev_curr < self.config.sigma_one_tolerance:
return self._first_order_update(x0, sample, sigma_curr, sigma_next, noise)
# Order-1 base.
x_next = self._first_order_update(x0, sample, sigma_curr, sigma_next, noise)
# Skip the higher-order term at the terminal step — the integral helpers diverge
# at lambda = 0 (sigma = 0), see _integral_one_over_fn docstring.
if sigma_next <= 0.0:
return x_next
alpha_curr, sigma_curr_t = self._sigma_to_alpha_sigma_t(sigma_curr)
alpha_next, sigma_next_t = self._sigma_to_alpha_sigma_t(sigma_next)
alpha_prev, sigma_prev_t = self._sigma_to_alpha_sigma_t(sigma_prev_curr)
lambda_curr = self._lambda(alpha_curr, sigma_curr_t)
lambda_next = self._lambda(alpha_next, sigma_next_t)
lambda_prev = self._lambda(alpha_prev, sigma_prev_t)
denom = lambda_curr - lambda_prev
if denom == 0.0:
return x_next
d_x0 = (x0 - old_x0) / denom
fn_next = _fn(lambda_next)
s_int = _integral_one_over_fn(lambda_next, lambda_curr)
x_next = x_next + alpha_next * (lambda_next - lambda_curr + s_int * fn_next) * d_x0
return x_next
def _third_order_update(
self,
sample: torch.Tensor,
sigma_curr: float,
sigma_next: float,
noise: Optional[torch.Tensor],
) -> torch.Tensor:
"""Order-3 ER-SDE step (ports ``vp_3_order_taylor``)."""
x0 = self.model_outputs[-1]
old_x0 = self.model_outputs[-2]
old_old_x0 = self.model_outputs[-3]
sigma_prev_curr = self._sigma_history[-2]
sigma_prev_prev = self._sigma_history[-3]
assert (
x0 is not None
and old_x0 is not None
and old_old_x0 is not None
and sigma_prev_curr is not None
and sigma_prev_prev is not None
)
# If any sigma in the lookback hits the boundary, fall back to order 2.
if self.config.use_flow_sigmas and (
1.0 - sigma_prev_curr < self.config.sigma_one_tolerance
or 1.0 - sigma_prev_prev < self.config.sigma_one_tolerance
):
return self._second_order_update(sample, sigma_curr, sigma_next, noise)
# Order-2 base.
x_next = self._second_order_update(sample, sigma_curr, sigma_next, noise)
if sigma_next <= 0.0:
return x_next
alpha_curr, sigma_curr_t = self._sigma_to_alpha_sigma_t(sigma_curr)
alpha_next, sigma_next_t = self._sigma_to_alpha_sigma_t(sigma_next)
alpha_prev, sigma_prev_t = self._sigma_to_alpha_sigma_t(sigma_prev_curr)
alpha_pprev, sigma_pprev_t = self._sigma_to_alpha_sigma_t(sigma_prev_prev)
lambda_curr = self._lambda(alpha_curr, sigma_curr_t)
lambda_next = self._lambda(alpha_next, sigma_next_t)
lambda_prev = self._lambda(alpha_prev, sigma_prev_t)
lambda_pprev = self._lambda(alpha_pprev, sigma_pprev_t)
denom_d = lambda_curr - lambda_prev
denom_d_prev = lambda_prev - lambda_pprev
denom_dd = lambda_curr - lambda_pprev
if denom_d == 0.0 or denom_d_prev == 0.0 or denom_dd == 0.0:
return x_next
d_x0 = (x0 - old_x0) / denom_d
old_d_x0 = (old_x0 - old_old_x0) / denom_d_prev
dd_x0 = 2.0 * (d_x0 - old_d_x0) / denom_dd
fn_next = _fn(lambda_next)
s_d_int = _integral_lam_minus_curr_over_fn(lambda_next, lambda_curr)
x_next = x_next + alpha_next * ((lambda_next - lambda_curr) ** 2 / 2.0 + s_d_int * fn_next) * dd_x0
return x_next
# ---- Public step ----------------------------------------------------------
def scale_model_input(
self, sample: torch.Tensor, timestep: Optional[Union[int, torch.Tensor]] = None
) -> torch.Tensor:
"""No-op (matches ``FlowMatchEulerDiscreteScheduler``)."""
return sample
def step(
self,
model_output: torch.Tensor,
timestep: Union[int, torch.Tensor],
sample: torch.Tensor,
generator: Optional[torch.Generator] = None,
return_dict: bool = True,
) -> Union[SchedulerOutput, Tuple]:
"""Predict the sample at the next timestep using one ER-SDE step."""
if self.num_inference_steps is None:
raise ValueError("num_inference_steps is None — call `set_timesteps` before calling `step`.")
if self.step_index is None:
self._init_step_index(timestep)
sigma_curr = float(self.sigmas[self.step_index].item())
sigma_next = float(self.sigmas[self.step_index + 1].item())
# 1. Convert model output to x0 prediction.
x0 = self._convert_model_output(model_output, sample)
# 2. FIFO-shift the multistep history. New entry goes in slot -1.
for i in range(self.config.solver_order - 1):
self.model_outputs[i] = self.model_outputs[i + 1]
self._sigma_history[i] = self._sigma_history[i + 1]
self.model_outputs[-1] = x0
self._sigma_history[-1] = sigma_curr
# 3. Sample noise (only when stochastic and not at terminal step).
if self.config.stochastic and sigma_next > 0.0:
noise = randn_tensor(
model_output.shape,
generator=generator,
device=model_output.device,
dtype=model_output.dtype,
)
else:
noise = None
# 4. Dispatch by available history.
if self.config.solver_order == 1 or self.lower_order_nums < 1:
prev_sample = self._first_order_update(x0, sample, sigma_curr, sigma_next, noise)
elif self.config.solver_order == 2 or self.lower_order_nums < 2:
prev_sample = self._second_order_update(sample, sigma_curr, sigma_next, noise)
else:
prev_sample = self._third_order_update(sample, sigma_curr, sigma_next, noise)
if self.lower_order_nums < self.config.solver_order:
self.lower_order_nums += 1
# 5. Advance step index.
self._step_index += 1
if not return_dict:
return (prev_sample,)
return SchedulerOutput(prev_sample=prev_sample)
# ---- Forward noising (training / img2img) ---------------------------------
def add_noise(
self,
original_samples: torch.Tensor,
noise: torch.Tensor,
timesteps: torch.Tensor,
) -> torch.Tensor:
"""Forward-noise ``original_samples`` at the given timesteps (img2img style)."""
sigmas = self.sigmas.to(device=original_samples.device, dtype=original_samples.dtype)
if original_samples.device.type == "mps" and torch.is_floating_point(timesteps):
schedule_timesteps = self.timesteps.to(original_samples.device, dtype=torch.float32)
timesteps = timesteps.to(original_samples.device, dtype=torch.float32)
else:
schedule_timesteps = self.timesteps.to(original_samples.device)
timesteps = timesteps.to(original_samples.device)
if self.begin_index is None:
step_indices = [self.index_for_timestep(t, schedule_timesteps) for t in timesteps]
elif self.step_index is not None:
step_indices = [self.step_index] * timesteps.shape[0]
else:
step_indices = [self.begin_index] * timesteps.shape[0]
sigma = sigmas[step_indices].flatten()
while len(sigma.shape) < len(original_samples.shape):
sigma = sigma.unsqueeze(-1)
if self.config.use_flow_sigmas:
alpha_t = 1.0 - sigma
sigma_t = sigma
else:
alpha_t = 1.0 / torch.sqrt(1.0 + sigma * sigma)
sigma_t = sigma * alpha_t
return alpha_t * original_samples + sigma_t * noise
def __len__(self) -> int:
return self.config.num_train_timesteps
@@ -0,0 +1,58 @@
import torch
def assert_broadcastable(*shapes):
try:
torch.broadcast_shapes(*shapes)
except RuntimeError as e:
raise AssertionError(f"Shapes {shapes} are not broadcastable.") from e
class RectifiedFlowInpaintExtension:
"""A class for managing inpainting with rectified flow models (e.g. FLUX, SD3, CogView4)."""
def __init__(self, init_latents: torch.Tensor, inpaint_mask: torch.Tensor, noise: torch.Tensor):
"""Initialize InpaintExtension.
Args:
init_latents (torch.Tensor): The initial latents (i.e. un-noised at timestep 0). In 'packed' format.
inpaint_mask (torch.Tensor): A mask specifying which elements to inpaint. Range [0, 1]. Values of 1 will be
re-generated. Values of 0 will remain unchanged. Values between 0 and 1 can be used to blend the
inpainted region with the background. In 'packed' format.
noise (torch.Tensor): The noise tensor used to noise the init_latents. In 'packed' format.
"""
assert_broadcastable(init_latents.shape, inpaint_mask.shape, noise.shape)
self._init_latents = init_latents
self._inpaint_mask = inpaint_mask
self._noise = noise
def _apply_mask_gradient_adjustment(self, t_prev: float) -> torch.Tensor:
"""Applies inpaint mask gradient adjustment and returns the inpaint mask to be used at the current timestep."""
# As we progress through the denoising process, we promote gradient regions of the mask to have a full weight of
# 1.0. This helps to produce more coherent seams around the inpainted region.
# We use a small epsilon to avoid any potential issues with floating point precision.
eps = 1e-4
mask = torch.where(self._inpaint_mask >= t_prev + eps, 1.0, 0.0).to(
dtype=self._inpaint_mask.dtype, device=self._inpaint_mask.device
)
return mask
def merge_intermediate_latents_with_init_latents(
self, intermediate_latents: torch.Tensor, t_prev: float
) -> torch.Tensor:
"""Merge the intermediate latents with the initial latents for the current timestep using the inpaint mask. I.e.
update the intermediate latents to keep the regions that are not being inpainted on the correct noise
trajectory.
This function should be called after each denoising step.
"""
mask = self._apply_mask_gradient_adjustment(t_prev)
# Noise the init latents for the current timestep.
noised_init_latents = self._noise * t_prev + (1.0 - t_prev) * self._init_latents
# Merge the intermediate latents with the noised_init_latents using the inpaint_mask.
return intermediate_latents * mask + noised_init_latents * (1.0 - mask)