Files
blepping-comfyui_overly_com…/py/step_samplers/base.py
T
blepping d8574bae2d Mega update (#8)
* Weird experiments with sampler blending

* Mega sync

* Use ComfyUI union types for wildcard inputs when available

* Add weoon wavelet sampler

* Fix noise sampler caching not considering immiscible settings
Allow specifying alt custom noise/immiscible settings for samplers that internally add noise
AFS support at the merge sampler level
Allow the Weoon internal step to use ETA
Add t_copysign expression function

* Start updating documentation

* Add ImmiscibleReference node, other changes

* Respect factor in ImmiscibleReference noise

* Fixing normalizing in ImmiscibleReference noise

* Handle case where there are no RGB factors (i.e. audio models)

* Fix rectified flow type detection for non-Flux models

* Add more tensor ops, add OCS ApplyExpressionImage/Latent nodes

* Rename ApplyExpression nodes to ApplyFilter, expression QoL improvements

* Fix apply filter nodes definitions

* Make t_noise handler work for image-latents.

* Add t_new_like expression function

* Make it possible to comment out lines with hash in expressions

* Add gradient estimation, pingpong and res_multistep samplers.
Add pingpong group merge method.
Remove model call caching stuff.
Allow defining pre_cfg and post_cfg filters.
Handle filters changing the latent shape better.
Allow disabling detecting out of order sigmas as restart sampling.

* Sync current updates (which I am too lazy to describe individually)

* Add ExpressionFilteredNoise node
Improvements to immiscible noise handling/expanded features
2025-08-09 07:28:10 -06:00

537 lines
17 KiB
Python

import contextlib
import typing
import torch
from . import registry # noqa: F401
from .. import filtering, noise, utils
from ..utils import fallback
class SamplerResult:
CLONE_KEYS = (
"denoised_cond",
"denoised_uncond",
"denoised",
"final",
"is_rectified_flow",
"noise_pred",
"noise_sampler",
"s_noise",
"sampler",
"sigma_down",
"sigma_next",
"sigma_up",
"sigma",
"step",
"substep",
"x_",
)
def __init__(
self,
ss,
sampler,
x,
sigma_up=None,
*,
split_result=None,
sigma=None,
sigma_next=None,
sigma_down=None,
s_noise=None,
noise_sampler=None,
final=True,
):
self.is_rectified_flow = ss.model.is_rectified_flow
self.sampler = sampler
self.sigma_up = fallback(sigma_up, ss.sigma.new_zeros(1))
self.s_noise = fallback(s_noise, sampler.s_noise)
self.sigma = fallback(sigma, ss.sigma)
self.sigma_next = fallback(sigma_next, ss.sigma_next)
self.sigma_down = fallback(sigma_down, self.sigma_next)
self.noise_sampler = fallback(noise_sampler, sampler.noise_sampler)
self.final = final
self.step = ss.step
self.substep = ss.substep
self.x_ = x
if split_result is not None:
self.denoised, self.noise_pred = split_result
elif x is None:
raise ValueError("SamplerResult requires at least one of x, split_result")
else:
self.denoised = self.noise_pred = None
_ = self.extract_pred(ss)
self.denoised_uncond = ss.hcur.denoised_uncond
self.denoised_cond = ss.hcur.denoised_cond
def get_noise(self, *, scaled=True, ss=None):
if self.sigma_next == 0 or self.noise_scale == 0:
return torch.zeros_like(self.x_)
return self.noise_sampler(
self.sigma,
self.sigma_next,
out_hw=self.x.shape[-2:],
x_ref=self.x,
refs=filtering.FilterRefs.from_sr(self) if ss is None else ss.refs,
).mul_(self.noise_scale if scaled else 1.0)
def extract_pred(self, ss):
if self.denoised is None or self.noise_pred is None:
self.denoised, self.noise_pred = utils.extract_pred(
ss.hcur.x, self.x_, ss.sigma, self.sigma_down
)
return self.denoised, self.noise_pred
@property
def x(self):
if self.x_ is None:
self.x_ = self.denoised + self.sigma_down * self.noise_pred
return self.x_
@property
def noise_scale(self):
return self.sigma_up * self.s_noise
def noise_x(self, x=None, scale=1.0, *, ss=None):
x = fallback(x, self.x)
if self.sigma_next == 0 or self.noise_scale == 0:
return x
noise = self.get_noise(ss=ss).mul_(scale)
if not self.is_rectified_flow:
return noise.add_(x)
x_coeff = (1 - self.sigma_next) / (1 - self.sigma_down)
# print(f"\nRF noise: {x_coeff}")
return noise.add_(x_coeff * x)
def clone(self):
obj = self.__new__(self.__class__)
for k in self.CLONE_KEYS:
if hasattr(self, k):
setattr(obj, k, getattr(self, k))
return obj
class StepSamplerContext:
def __init__(self, sampler, *args, **kwargs):
self.sampler = sampler
self.args = args
self.kwargs = kwargs
def __enter__(self):
if self.sampler.ss is not None:
raise RuntimeError("Cannot reenter prepared sampler in context manager!")
self.sampler.prepare(*self.args, **self.kwargs)
return self.sampler
def __exit__(self, *_unused):
self.sampler.reset()
class SingleStepSampler:
name = None
self_noise = 0
model_calls = 0
ancestralize = False
sample_sigma_zero = False
immiscible = None
allow_cfgpp = False
allow_alt_cfgpp = False
afs_end_step = -1
uses_alt_noise = False
default_eta = 1.0
def __init__(
self,
*,
noise_sampler=None,
substeps=1,
s_noise=1.0,
eta=None,
eta_retry_increment=0,
dyn_eta_start=None,
dyn_eta_end=None,
weight=1.0,
pre_filter=None,
post_filter=None,
immiscible=None,
**kwargs,
):
self.ss = None
self.options = kwargs
self.cfgpp = self.allow_cfgpp and self.options.pop("cfgpp", False) is True
alt_cfgpp_scale = self.options.pop("alt_cfgpp_scale", 0.0)
self.alt_cfgpp_scale = 0.0 if not self.allow_alt_cfgpp else alt_cfgpp_scale
self.s_noise = s_noise
self.eta = fallback(eta, self.default_eta)
self.eta_retry_increment = eta_retry_increment
self.dyn_eta_start = dyn_eta_start
self.dyn_eta_end = dyn_eta_end
self.noise_sampler = noise_sampler
self.immiscible = (
noise.ImmiscibleNoise(**immiscible)
if immiscible not in (False, None)
else immiscible
)
self.weight = weight
self.afs_end_step = self.options.pop("afs_end_step", -1)
self.substeps = substeps
self.pre_filter = (
None if pre_filter is None else filtering.make_filter(pre_filter)
)
self.post_filter = (
None if post_filter is None else filtering.make_filter(post_filter)
)
self.custom_noise = self.options.get("custom_noise")
if isinstance(self.custom_noise, str):
self.custom_noise = self.options.get(f"custom_noise_{self.custom_noise}")
if not self.uses_alt_noise:
return
self.alt_custom_noise = self.options.get("custom_noise_alt")
alt_immiscible = self.options.get("alt_immiscible")
self.alt_immiscible = (
noise.ImmiscibleNoise(**alt_immiscible)
if isinstance(alt_immiscible, dict)
else alt_immiscible
)
def __call__(self, x):
ss = self.ss
orig_x = x
if not self.sample_sigma_zero and ss.sigma_next == 0:
return (yield from self.denoised_result())
if ss.step <= self.afs_end_step:
return (yield from self.afs_step(x))
if self.pre_filter or self.post_filter:
filter_refs = ss.refs | filtering.FilterRefs({"orig_x": orig_x})
if self.pre_filter:
x = self.pre_filter.apply(x, refs=filter_refs)
next_x = None
sg = self.step(x)
with contextlib.suppress(StopIteration):
while True:
sr = sg.send(next_x)
if sr.final:
if self.ancestralize:
sr = self.ancestralize_result(sr)
curr_x = sr.x
if self.post_filter:
curr_x = self.post_filter.apply(curr_x, refs=filter_refs)
sr.x_ = curr_x
return (yield sr)
next_x = sr.noise_x(ss=ss)
def step(self, x):
raise NotImplementedError
def prepare(self, ss):
self.ss = ss
self.noise_sampler = ss.noise.make_caching_noise_sampler(
self.custom_noise,
self.max_noise_samples,
ss.sigma,
ss.sigma_next,
immiscible=fallback(self.immiscible, ss.noise.immiscible),
)
if not self.uses_alt_noise:
return
if self.alt_custom_noise is None and self.alt_immiscible is None:
self.alt_noise_sampler = self.noise_sampler
return
self.alt_noise_sampler = ss.noise.make_caching_noise_sampler(
fallback(self.alt_custom_noise, self.custom_noise),
1,
ss.sigma,
ss.sigma_next,
immiscible=fallback(
fallback(self.alt_immiscible, self.immiscible),
ss.noise.immiscible,
),
)
def reset(self):
self.ss = None
self.noise_sampler = None
if self.uses_alt_noise:
self.alt_noise_sampler = None
# From https://arxiv.org/abs/2210.05475
def afs_step(self, x):
sigma, sigma_next = self.ss.sigma, self.ss.sigma_next
afs_d = x / ((1 + sigma**2).sqrt())
dt = sigma_next - sigma
return (yield from self.result(x + afs_d * dt))
# Euler - based on original ComfyUI implementation
def euler_step(
self,
x,
*,
sigma_down=None,
sigma_up=None,
eta=None,
sigma=None,
sigma_next=None,
):
eta = fallback(eta, self.get_dyn_eta())
if sigma_down is None or sigma_up is None:
if not (sigma_down is None and sigma_up is None):
raise ValueError("Must pass both sigma_down and sigma_up or neither")
sigma_down, sigma_up = self.get_ancestral_step(
eta=eta, sigma=sigma, sigma_next=sigma_next
)
return (
yield from self.split_result(
*self.get_split_prediction(), sigma_down=sigma_down, sigma_up=sigma_up
)
)
def denoised_result(self, **kwargs):
ss = self.ss
return (
yield SamplerResult(ss, self, ss.denoised, ss.sigma.new_zeros(1), **kwargs)
)
def result(self, x, noise_scale=None, **kwargs):
return (yield SamplerResult(self.ss, self, x, noise_scale, **kwargs))
def split_result(
self, denoised=None, noise_pred=None, sigma_up=None, sigma_down=None, **kwargs
):
return (
yield SamplerResult(
ss=self.ss,
sampler=self,
x=None,
sigma_up=sigma_up,
sigma_down=sigma_down,
split_result=(denoised, noise_pred),
**kwargs,
)
)
def get_ancestral_step(
self, *args, dyn_eta=False, as_dict=False, retry_increment=None, **kwargs
):
if dyn_eta:
args = (self.get_dyn_eta(), *args)
retry_increment = fallback(retry_increment, self.eta_retry_increment)
sigma_down, sigma_up = self.ss.get_ancestral_step(
*args, retry_increment=retry_increment, **kwargs
)
if not as_dict:
return sigma_down, sigma_up
return {"sigma_down": sigma_down, "sigma_up": sigma_up}
def ancestralize_result(self, sr):
ss = self.ss
new_sr = sr.clone()
if new_sr.sigma_down is not None and new_sr.sigma_down != new_sr.sigma_next:
return sr
eta = self.get_dyn_eta()
if sr.sigma_next == 0 or eta == 0:
return sr
sd, su = self.get_ancestral_step(eta, sigma=sr.sigma, sigma_next=sr.sigma_next)
_ = new_sr.extract_pred(ss)
new_sr.x_ = None
new_sr.sigma_up = su
new_sr.sigma_down = sd
return new_sr
def __str__(self):
return f"<SS({self.name}): s_noise={self.s_noise}, eta={self.eta}>"
def get_dyn_value(self, start, end):
if None in (start, end):
return 1.0
if start == end:
return start
ss = self.ss
main_idx = getattr(ss, "main_idx", ss.idx)
main_sigmas = getattr(ss, "main_sigmas", ss.sigmas)
step_pct = main_idx / (len(main_sigmas) - 1)
dd_diff = end - start
return start + dd_diff * step_pct
def get_dyn_eta(self):
return self.eta * self.get_dyn_value(self.dyn_eta_start, self.dyn_eta_end)
@property
def max_noise_samples(self):
return (1 + self.self_noise) * self.substeps
@property
def require_uncond(self):
return self.cfgpp or self.alt_cfgpp_scale != 0
def to_d(self, mr, *, use_cfgpp=True, **kwargs):
if not use_cfgpp:
return mr.to_d(**kwargs)
return mr.to_d(alt_cfgpp_scale=self.alt_cfgpp_scale, cfgpp=self.cfgpp, **kwargs)
def get_split_prediction(self, *, mr=None, sigma=None, **kwargs):
mr = fallback(mr, self.ss.hcur)
sigma = fallback(sigma, mr.sigma)
return mr.get_split_prediction(
sigma=sigma,
alt_cfgpp_scale=self.alt_cfgpp_scale,
cfgpp=self.cfgpp,
**kwargs,
)
def call_model(self, *args, **kwargs):
ss = self.ss
kwargs["require_uncond"] = self.require_uncond or kwargs.get(
"require_uncond", False
)
kwargs["cfg_scale_override"] = kwargs.get(
"cfg_scale_override",
self.options.get("cfg_scale_override", ss.cfg_scale_override),
)
return ss.call_model(*args, ss=ss, **kwargs)
def step_mix(self, x, denoised, uncond, ratio, *, blend=torch.lerp):
if self.cfgpp:
return denoised + (x - uncond).mul_(ratio)
pp = self.alt_cfgpp_scale
if pp == 0:
return blend(denoised, x, ratio)
return blend(denoised * (1 + pp) - uncond * pp, x, ratio)
class HistorySingleStepSampler(SingleStepSampler):
default_history_limit, max_history = 0, 0
def __init__(self, *args, history_limit=None, **kwargs):
super().__init__(*args, **kwargs)
self.history_limit = min(
self.max_history,
max(
0,
self.default_history_limit if history_limit is None else history_limit,
),
)
def available_history(self):
ss = self.ss
available = max(
0, min(ss.idx, self.history_limit, self.max_history, len(ss.hist) - 1)
)
if not available:
return available
curr_shape = ss.hist[-1].denoised.shape
for eff_available in range(available):
if ss.hist[-2 - eff_available].denoised.shape != curr_shape:
return eff_available
return available
class ReversibleConfig(typing.NamedTuple):
scale: float
eta: float
dyn_eta_start: float | None = None
dyn_eta_end: float | None = None
eta_retry_increment: float = 0.0
start_step: int = 0
end_step: int = 9999
use_cfgpp: bool = False
@classmethod
def build(cls, *, default_eta, default_scale, eta=None, scale=None, **kwargs):
return cls.__new__(
cls,
eta=fallback(eta, default_eta),
scale=fallback(scale, default_scale),
**kwargs,
)
def check(self, step):
return self.scale != 0 and self.start_step <= step <= self.end_step
class ReversibleSingleStepSampler(HistorySingleStepSampler):
default_reversible_scale = 1.0
default_reta = 1.0
def __init__(
self,
*,
reversible_scale=None,
reta=None,
dyn_reta_start=None,
dyn_reta_end=None,
reversible_start_step=0,
reversible=None,
**kwargs,
):
super().__init__(**kwargs)
if reversible is None:
# For backward compatibility.
self.reversible = ReversibleConfig.build(
default_eta=self.default_reta,
default_scale=self.default_reversible_scale,
scale=reversible_scale,
eta=reta,
dyn_eta_start=dyn_reta_start,
dyn_eta_end=dyn_reta_end,
start_step=reversible_start_step,
)
return
self.reversible = ReversibleConfig.build(
default_eta=self.default_reta,
default_scale=self.default_reversible_scale,
**reversible,
)
def reversible_correction(self):
raise NotImplementedError
def get_dyn_reta(self, *, r=None):
r = fallback(r, self.reversible)
ss = self.ss
if not r.check(ss.step):
return 0.0
return r.eta * self.get_dyn_value(r.dyn_eta_start, r.dyn_eta_end)
dyn_reta = property(get_dyn_reta)
def get_reversible_cfg(self, *, reversible=None):
reversible = fallback(reversible, self.reversible)
ss = self.ss
if not reversible.check(ss.step):
return 0.0, 0.0
return self.get_dyn_reta(r=reversible), reversible.scale
class DPMPPStepMixin:
@staticmethod
def sigma_fn(t):
return t.neg().exp()
@staticmethod
def t_fn(t):
return t.log().neg()
class MinSigmaStepMixin:
@staticmethod
def adjust_step(sigma, min_sigma, threshold=5e-04):
if min_sigma - sigma > threshold:
return sigma.clamp(min=min_sigma)
return sigma
def adjusted_step(self, sn, result, mcc, sigma_up):
ss = self.ss
if sn == ss.sigma_next:
return sigma_up, result
# FIXME: Make sure we're noising from the right sigma.
result = yield from self.result(
result, sigma_up, sigma=ss.sigma, sigma_next=sn, final=False
)
mr = self.call_model(result, sn, call_index=mcc)
dt = ss.sigma_next - sn
result = result + self.to_d(mr) * dt
return sigma_up.new_zeros(1), result