Spaces:
Running on Zero
Running on Zero
Upload pk_workflow.py
Browse files- pk_workflow.py +470 -0
pk_workflow.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The three things that make `Plaguekind/Minimax-H3` a *workflow* rather than just MiniMax-H3, ported onto the
|
| 2 |
+
`ref2va` Space.
|
| 3 |
+
|
| 4 |
+
Trimmed from the fl2va Space's `pk_workflow.py`: only the plain `scheduler.step()` samplers are here —
|
| 5 |
+
`euler` (no patch at all), `euler_ancestral`, and `er_sde`. The SDE-family samplers (`dpmpp_2m_sde_gpu`,
|
| 6 |
+
`dpmpp_3m_sde_gpu`) and the two-evaluation-per-step samplers (`dpmpp_2s_ancestral`, `dpmpp_sde_gpu`, `seeds_2`,
|
| 7 |
+
in `h3_dpmpp_2s_ancestral.py` on the fl2va Space) aren't ported here, so neither is `torchsde` or the Brownian-tree
|
| 8 |
+
noise machinery either sampler family needs.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# ----------------------------------------------------------------------------------------------------------------
|
| 17 |
+
# BasicScheduler(linear_quadratic)
|
| 18 |
+
# ----------------------------------------------------------------------------------------------------------------
|
| 19 |
+
# MiniMax-H3 carries two rectified-flow schedules per request, `shift = 12` for the video rows and `shift = 3` for
|
| 20 |
+
# the audio rows. diffusers builds both from one `linspace(1, 0, steps)` base grid; ComfyUI instead samples the
|
| 21 |
+
# *video* schedule and derives the audio one from it in closed form
|
| 22 |
+
# (`comfy/ldm/minimax/model.py::time_shift_sigma`). The two agree, because the shift is a bijection of the base
|
| 23 |
+
# grid — which is what lets a schedule chosen in ComfyUI's video-sigma space be transplanted here exactly.
|
| 24 |
+
#
|
| 25 |
+
# `linear_quadratic` is Mochi's schedule (`comfy/samplers.py::linear_quadratic_schedule`) and it does **not** go
|
| 26 |
+
# through the model's shift at all: it is `sigma_max = 1.0` scaled, so the grid PlagueKind's 15 steps actually run
|
| 27 |
+
# is this one verbatim, in the video stream, with the audio stream shifted off it.
|
| 28 |
+
VIDEO_SHIFT = 12.0
|
| 29 |
+
AUDIO_SHIFT = 3.0
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def linear_quadratic_sigmas(
|
| 33 |
+
steps: int, threshold_noise: float = 0.025, linear_steps: int | None = None
|
| 34 |
+
) -> torch.Tensor:
|
| 35 |
+
"""ComfyUI's `linear_quadratic` sigma grid, in MiniMax-H3's video-sigma space.
|
| 36 |
+
|
| 37 |
+
Ported from `comfy/samplers.py::linear_quadratic_schedule` (itself from Mochi), with
|
| 38 |
+
`model_sampling.sigma_max == 1.0`, which is what a rectified-flow model has. Returns `steps + 1` strictly
|
| 39 |
+
decreasing sigmas from exactly 1.0 to exactly 0.0, so it drives `steps` forwards — ComfyUI's step count, not
|
| 40 |
+
diffusers' (where the terminal zero is one of the `num_inference_steps`).
|
| 41 |
+
|
| 42 |
+
Half the steps crawl through the first 2.5% of the trajectory and the rest sprint the remaining 97.5%: it is a
|
| 43 |
+
front-loaded schedule, which is why 15 steps of it hold up against ~28 of the native grid.
|
| 44 |
+
"""
|
| 45 |
+
steps = int(steps)
|
| 46 |
+
if steps < 2:
|
| 47 |
+
return torch.tensor([1.0, 0.0], dtype=torch.float32)
|
| 48 |
+
if linear_steps is None:
|
| 49 |
+
linear_steps = steps // 2
|
| 50 |
+
|
| 51 |
+
linear = [i * threshold_noise / linear_steps for i in range(linear_steps)]
|
| 52 |
+
threshold_noise_step_diff = linear_steps - threshold_noise * steps
|
| 53 |
+
quadratic_steps = steps - linear_steps
|
| 54 |
+
quadratic_coef = threshold_noise_step_diff / (linear_steps * quadratic_steps**2)
|
| 55 |
+
linear_coef = threshold_noise / linear_steps - 2 * threshold_noise_step_diff / (quadratic_steps**2)
|
| 56 |
+
const = quadratic_coef * (linear_steps**2)
|
| 57 |
+
quadratic = [quadratic_coef * (i**2) + linear_coef * i + const for i in range(linear_steps, steps)]
|
| 58 |
+
|
| 59 |
+
schedule = linear + quadratic + [1.0]
|
| 60 |
+
return torch.tensor([1.0 - value for value in schedule], dtype=torch.float32)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def time_shift_sigma(sigma: torch.Tensor, from_shift: float, to_shift: float) -> torch.Tensor:
|
| 64 |
+
"""Move a sigma between two exponential shifts of the same base grid.
|
| 65 |
+
|
| 66 |
+
`comfy/ldm/minimax/model.py::time_shift_sigma`: invert `sigma = s*b / (1 + (s-1)*b)` back to the base grid `b`,
|
| 67 |
+
then re-apply the other shift. Monotonic, and it fixes both 0.0 and 1.0, so a strictly decreasing schedule that
|
| 68 |
+
ends at zero stays one.
|
| 69 |
+
"""
|
| 70 |
+
if from_shift == to_shift:
|
| 71 |
+
return sigma
|
| 72 |
+
base = sigma / (from_shift + sigma * (1.0 - from_shift))
|
| 73 |
+
return to_shift * base / (1.0 + (to_shift - 1.0) * base)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
# ----------------------------------------------------------------------------------------------------------------
|
| 77 |
+
# BasicScheduler(sgm_uniform / simple / beta / ddim_uniform / normal)
|
| 78 |
+
# ----------------------------------------------------------------------------------------------------------------
|
| 79 |
+
# Five more of ComfyUI's `BasicScheduler` names, ported from `comfy/samplers.py`. Each is computed at the
|
| 80 |
+
# *reference* shift (1.0 — where `time_snr_shift` is the identity, so `sigma(t) == t`) and reprojected onto each
|
| 81 |
+
# scheduler's real shift by `time_shift_sigma`, exactly like `linear_quadratic_sigmas` already is and for the same
|
| 82 |
+
# reason: it keeps the video and audio streams pinned to the same underlying denoising progress at each step,
|
| 83 |
+
# which computing each stream's schedule independently at its own shift would not.
|
| 84 |
+
#
|
| 85 |
+
# `FLOW_TIMESTEPS` mirrors ComfyUI's `ModelSamplingDiscreteFlow`/`ModelSamplingAV` default of 1000 discrete steps
|
| 86 |
+
# (`comfy/model_sampling.py`). Unverified specifically for MiniMax-H3's own `sampling_settings` — if a ported
|
| 87 |
+
# schedule's shape looks visibly different from ComfyUI's own render at the same steps/seed, this is the first
|
| 88 |
+
# thing to check.
|
| 89 |
+
FLOW_TIMESTEPS = 1000
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _reference_sigma(index_1based: int) -> float:
|
| 93 |
+
"""`ModelSamplingAV.sigma(timestep)` at shift == 1.0: the shift formula is the identity, so this is just the
|
| 94 |
+
plain fraction `index / FLOW_TIMESTEPS`. `index_1based` matches ComfyUI's 1-based table construction
|
| 95 |
+
(`torch.arange(1, timesteps + 1) / timesteps`)."""
|
| 96 |
+
return index_1based / FLOW_TIMESTEPS
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def sgm_uniform_sigmas(steps: int) -> torch.Tensor:
|
| 100 |
+
"""ComfyUI's `sgm_uniform`. Uniform in *timestep* space between the max and min sigma, dropping the point
|
| 101 |
+
that would land exactly on the minimum, then appending an exact 0.0. `steps + 1` sigmas."""
|
| 102 |
+
steps = int(steps)
|
| 103 |
+
timesteps = torch.linspace(float(FLOW_TIMESTEPS), 1.0, steps + 1)[:-1]
|
| 104 |
+
sigmas = (timesteps / FLOW_TIMESTEPS).tolist() + [0.0]
|
| 105 |
+
return torch.tensor(sigmas, dtype=torch.float32)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def normal_sigmas(steps: int) -> torch.Tensor:
|
| 109 |
+
"""ComfyUI's `normal`. Same idea as `sgm_uniform` but the linspace includes both endpoints (the minimum
|
| 110 |
+
sigma is reached exactly, not dropped), with 0.0 still appended."""
|
| 111 |
+
steps = int(steps)
|
| 112 |
+
timesteps = torch.linspace(float(FLOW_TIMESTEPS), 1.0, steps)
|
| 113 |
+
sigmas = (timesteps / FLOW_TIMESTEPS).tolist() + [0.0]
|
| 114 |
+
return torch.tensor(sigmas, dtype=torch.float32)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def simple_sigmas(steps: int) -> torch.Tensor:
|
| 118 |
+
"""ComfyUI's `simple`: evenly-spaced *indices* into the 1000-entry sigma table, walked from the high-noise
|
| 119 |
+
end, then 0.0 appended."""
|
| 120 |
+
steps = int(steps)
|
| 121 |
+
stride = FLOW_TIMESTEPS / steps
|
| 122 |
+
sigmas = [_reference_sigma(FLOW_TIMESTEPS - int(x * stride)) for x in range(steps)]
|
| 123 |
+
sigmas.append(0.0)
|
| 124 |
+
return torch.tensor(sigmas, dtype=torch.float32)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def ddim_uniform_sigmas(steps: int) -> torch.Tensor:
|
| 128 |
+
"""ComfyUI's `ddim_uniform`: a fixed-stride walk through the sigma table starting one index in, reversed so
|
| 129 |
+
the highest sigma comes first, ending at 0.0."""
|
| 130 |
+
steps = int(steps)
|
| 131 |
+
stride = max(FLOW_TIMESTEPS // steps, 1)
|
| 132 |
+
sigmas = [0.0]
|
| 133 |
+
index = 1
|
| 134 |
+
while index < FLOW_TIMESTEPS:
|
| 135 |
+
sigmas.append(_reference_sigma(index))
|
| 136 |
+
index += stride
|
| 137 |
+
sigmas.reverse()
|
| 138 |
+
return torch.tensor(sigmas, dtype=torch.float32)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def beta_sigmas(steps: int, alpha: float = 0.6, beta: float = 0.6) -> torch.Tensor:
|
| 142 |
+
"""ComfyUI's `beta` (arxiv.org/abs/2407.12173): table indices drawn from a Beta(alpha, beta) inverse CDF
|
| 143 |
+
instead of an even stride, biasing samples toward one end of the trajectory. Needs `scipy`."""
|
| 144 |
+
import numpy
|
| 145 |
+
import scipy.stats
|
| 146 |
+
|
| 147 |
+
steps = int(steps)
|
| 148 |
+
total = FLOW_TIMESTEPS - 1
|
| 149 |
+
positions = 1.0 - numpy.linspace(0.0, 1.0, steps, endpoint=False)
|
| 150 |
+
indices = numpy.rint(scipy.stats.beta.ppf(positions, alpha, beta) * total)
|
| 151 |
+
sigmas = []
|
| 152 |
+
last = -1
|
| 153 |
+
for value in indices:
|
| 154 |
+
if value != last:
|
| 155 |
+
sigmas.append(_reference_sigma(int(value) + 1))
|
| 156 |
+
last = value
|
| 157 |
+
sigmas.append(0.0)
|
| 158 |
+
return torch.tensor(sigmas, dtype=torch.float32)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
SCHEDULE_SIGMA_FUNCS = {
|
| 162 |
+
"linear_quadratic": linear_quadratic_sigmas,
|
| 163 |
+
"sgm_uniform": sgm_uniform_sigmas,
|
| 164 |
+
"simple": simple_sigmas,
|
| 165 |
+
"beta": beta_sigmas,
|
| 166 |
+
"ddim_uniform": ddim_uniform_sigmas,
|
| 167 |
+
"normal": normal_sigmas,
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def _euler_ancestral_step(scheduler, generator, model_output, timestep, sample, eta: float = 1.0, s_noise: float = 1.0):
|
| 172 |
+
"""Ports k-diffusion's `sample_euler_ancestral_RF` — the flow-matching branch `sample_euler_ancestral`
|
| 173 |
+
dispatches to for `CONST`-style model sampling, which is what MiniMax-H3's `[0, 1]` sigma space is — onto one
|
| 174 |
+
`MiniMaxH3Scheduler.step()` call. Single model evaluation, same shape as `step()` itself, with fresh
|
| 175 |
+
ancestral noise injected each step instead of a plain Euler blend. Mirrors `step()`'s own care around
|
| 176 |
+
recomputing `sigma_from_timestep` from `timestep` rather than reading `self.sigmas` at the current index, for
|
| 177 |
+
the same numerical-consistency reason documented there.
|
| 178 |
+
"""
|
| 179 |
+
if scheduler._step_index is None:
|
| 180 |
+
scheduler._step_index = scheduler.index_for_timestep(timestep) if scheduler._begin_index is None else scheduler._begin_index
|
| 181 |
+
|
| 182 |
+
if not isinstance(timestep, torch.Tensor):
|
| 183 |
+
timestep = torch.tensor(timestep, dtype=sample.dtype)
|
| 184 |
+
sigma_from_timestep = 1 - timestep.to(device=sample.device, dtype=sample.dtype)
|
| 185 |
+
while sigma_from_timestep.ndim < sample.ndim:
|
| 186 |
+
sigma_from_timestep = sigma_from_timestep.unsqueeze(-1)
|
| 187 |
+
denoised = sample + sigma_from_timestep * model_output
|
| 188 |
+
|
| 189 |
+
compute_dtype = torch.float32 if sample.dtype in (torch.float16, torch.bfloat16) else sample.dtype
|
| 190 |
+
sigma = scheduler.sigmas[scheduler._step_index].to(device=sample.device, dtype=compute_dtype)
|
| 191 |
+
sigma_next = scheduler.sigmas[scheduler._step_index + 1].to(device=sample.device, dtype=compute_dtype)
|
| 192 |
+
x = sample.to(dtype=compute_dtype)
|
| 193 |
+
denoised = denoised.to(dtype=compute_dtype)
|
| 194 |
+
|
| 195 |
+
if sigma_next == 0:
|
| 196 |
+
prev_sample = denoised
|
| 197 |
+
else:
|
| 198 |
+
downstep_ratio = 1 + (sigma_next / sigma - 1) * eta
|
| 199 |
+
sigma_down = sigma_next * downstep_ratio
|
| 200 |
+
alpha_next = 1 - sigma_next
|
| 201 |
+
alpha_down = 1 - sigma_down
|
| 202 |
+
renoise_coeff = (sigma_next**2 - sigma_down**2 * alpha_next**2 / alpha_down**2).clamp_min(0).sqrt()
|
| 203 |
+
ratio = sigma_down / sigma
|
| 204 |
+
prev_sample = ratio * x + (1 - ratio) * denoised
|
| 205 |
+
if eta > 0:
|
| 206 |
+
noise = torch.randn(x.shape, dtype=x.dtype, device="cpu", generator=generator).to(x.device)
|
| 207 |
+
prev_sample = (alpha_next / alpha_down) * prev_sample + noise * s_noise * renoise_coeff
|
| 208 |
+
|
| 209 |
+
prev_sample = prev_sample.to(dtype=sample.dtype)
|
| 210 |
+
scheduler._step_index += 1
|
| 211 |
+
return prev_sample
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def _er_sde_step(scheduler, generator, model_output, timestep, sample, s_noise: float = 1.0, max_stage: int = 3):
|
| 215 |
+
"""Ports k-diffusion's `sample_er_sde` (VP ER-SDE-Solver-3, arXiv:2309.06169) onto one
|
| 216 |
+
`MiniMaxH3Scheduler.step()` call. Single model evaluation per step — second/third-order accuracy comes from
|
| 217 |
+
the previous one or two steps' denoised estimates, not an extra evaluation this step — so it carries history
|
| 218 |
+
on the scheduler instance across calls, reset each request by `use_schedule` alongside `_step_index`.
|
| 219 |
+
"""
|
| 220 |
+
if scheduler._step_index is None:
|
| 221 |
+
scheduler._step_index = scheduler.index_for_timestep(timestep) if scheduler._begin_index is None else scheduler._begin_index
|
| 222 |
+
i = scheduler._step_index
|
| 223 |
+
|
| 224 |
+
if not isinstance(timestep, torch.Tensor):
|
| 225 |
+
timestep = torch.tensor(timestep, dtype=sample.dtype)
|
| 226 |
+
sigma_from_timestep = 1 - timestep.to(device=sample.device, dtype=sample.dtype)
|
| 227 |
+
while sigma_from_timestep.ndim < sample.ndim:
|
| 228 |
+
sigma_from_timestep = sigma_from_timestep.unsqueeze(-1)
|
| 229 |
+
denoised = sample + sigma_from_timestep * model_output
|
| 230 |
+
|
| 231 |
+
compute_dtype = torch.float32 if sample.dtype in (torch.float16, torch.bfloat16) else sample.dtype
|
| 232 |
+
sigmas = scheduler.sigmas.to(device=sample.device, dtype=compute_dtype)
|
| 233 |
+
sigma, sigma_next = sigmas[i], sigmas[i + 1]
|
| 234 |
+
x = sample.to(dtype=compute_dtype)
|
| 235 |
+
denoised = denoised.to(dtype=compute_dtype)
|
| 236 |
+
|
| 237 |
+
if i == 0 and float(sigma) >= 1.0:
|
| 238 |
+
# `1 - sigma` sits in a denominator below; MiniMax-H3's first sigma is exactly 1.0, so nudge it a hair
|
| 239 |
+
# under 1.0 for this sampler's math only, matching ComfyUI's `offset_first_sigma_for_snr`. Does not
|
| 240 |
+
# touch `sigma_from_timestep` above — the model was still conditioned on the real timestep.
|
| 241 |
+
base = torch.tensor(1.0 - 1e-4, dtype=compute_dtype, device=sample.device)
|
| 242 |
+
shift = float(scheduler.shift)
|
| 243 |
+
sigma = shift * base / (1 + (shift - 1) * base)
|
| 244 |
+
|
| 245 |
+
def er_lambda(s):
|
| 246 |
+
return s / (1 - s)
|
| 247 |
+
|
| 248 |
+
def noise_scaler(v):
|
| 249 |
+
return v * (v**0.3).exp() + v * 10.0
|
| 250 |
+
|
| 251 |
+
if sigma_next == 0:
|
| 252 |
+
prev_sample = denoised
|
| 253 |
+
else:
|
| 254 |
+
er_lambda_s, er_lambda_t = er_lambda(sigma), er_lambda(sigma_next)
|
| 255 |
+
alpha_s, alpha_t = 1 - sigma, 1 - sigma_next
|
| 256 |
+
r_alpha = alpha_t / alpha_s
|
| 257 |
+
r = noise_scaler(er_lambda_t) / noise_scaler(er_lambda_s)
|
| 258 |
+
|
| 259 |
+
prev_sample = r_alpha * r * x + alpha_t * (1 - r) * denoised
|
| 260 |
+
|
| 261 |
+
stage_used = min(max_stage, i + 1)
|
| 262 |
+
if stage_used >= 2:
|
| 263 |
+
num_points = 200
|
| 264 |
+
dt = er_lambda_t - er_lambda_s
|
| 265 |
+
step_size = -dt / num_points
|
| 266 |
+
positions = er_lambda_t + torch.arange(num_points, device=x.device, dtype=compute_dtype) * step_size
|
| 267 |
+
scaled = noise_scaler(positions)
|
| 268 |
+
|
| 269 |
+
s_term = torch.sum(1 / scaled) * step_size
|
| 270 |
+
er_lambda_prev = er_lambda(sigmas[i - 1])
|
| 271 |
+
denoised_d = (denoised - scheduler._er_sde_old_denoised) / (er_lambda_s - er_lambda_prev)
|
| 272 |
+
prev_sample = prev_sample + alpha_t * (dt + s_term * noise_scaler(er_lambda_t)) * denoised_d
|
| 273 |
+
|
| 274 |
+
if stage_used >= 3:
|
| 275 |
+
s_u_term = torch.sum((positions - er_lambda_s) / scaled) * step_size
|
| 276 |
+
er_lambda_prev2 = er_lambda(sigmas[i - 2])
|
| 277 |
+
denoised_u = (denoised_d - scheduler._er_sde_old_denoised_d) / ((er_lambda_s - er_lambda_prev2) / 2)
|
| 278 |
+
prev_sample = prev_sample + alpha_t * ((dt**2) / 2 + s_u_term * noise_scaler(er_lambda_t)) * denoised_u
|
| 279 |
+
scheduler._er_sde_old_denoised_d = denoised_d
|
| 280 |
+
|
| 281 |
+
if s_noise > 0:
|
| 282 |
+
noise = torch.randn(x.shape, dtype=x.dtype, device="cpu", generator=generator).to(x.device)
|
| 283 |
+
spread = (er_lambda_t**2 - er_lambda_s**2 * r**2).clamp_min(0).sqrt()
|
| 284 |
+
prev_sample = prev_sample + alpha_t * noise * s_noise * spread
|
| 285 |
+
|
| 286 |
+
scheduler._er_sde_old_denoised = denoised
|
| 287 |
+
prev_sample = prev_sample.to(dtype=sample.dtype)
|
| 288 |
+
scheduler._step_index += 1
|
| 289 |
+
return prev_sample
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
class use_schedule:
|
| 293 |
+
"""Set each scheduler's shift for one request, and — for anything but `native` — force its sigma grid onto
|
| 294 |
+
one of `SCHEDULE_SIGMA_FUNCS`'s named schedules.
|
| 295 |
+
|
| 296 |
+
`MiniMaxH3Scheduler.shift` is a read-only property, so a different shift means swapping in a freshly built
|
| 297 |
+
scheduler via `from_config(..., shift=...)` rather than mutating one in place — the standard diffusers idiom
|
| 298 |
+
for changing a `ConfigMixin` parameter after construction, and correct regardless of exactly how `shift` is
|
| 299 |
+
stored internally. Applied unconditionally, including under `native`, so the shift sliders affect the
|
| 300 |
+
pipeline's own default schedule too — and always restored on exit, since `pipe.scheduler`/`pipe.audio_scheduler`
|
| 301 |
+
are shared, request-spanning objects that must not carry one request's shift into the next.
|
| 302 |
+
"""
|
| 303 |
+
|
| 304 |
+
def __init__(self, pipe, steps: int, schedule_name: str, video_shift: float, audio_shift: float, sampler_name: str = "euler", seed: int = 0, threshold_noise: float = 0.025):
|
| 305 |
+
self.pipe = pipe
|
| 306 |
+
self.attr_names = ["scheduler", "audio_scheduler"]
|
| 307 |
+
self.shifts = [float(video_shift), float(audio_shift)]
|
| 308 |
+
self.schedule_name = schedule_name
|
| 309 |
+
self.sampler_name = sampler_name
|
| 310 |
+
self.seed = int(seed)
|
| 311 |
+
self.steps = int(steps)
|
| 312 |
+
self.threshold_noise = float(threshold_noise)
|
| 313 |
+
self._originals: dict = {}
|
| 314 |
+
|
| 315 |
+
def __enter__(self):
|
| 316 |
+
for attr_name, shift in zip(self.attr_names, self.shifts):
|
| 317 |
+
original = getattr(self.pipe, attr_name)
|
| 318 |
+
self._originals[attr_name] = original
|
| 319 |
+
if float(original.shift) != shift:
|
| 320 |
+
setattr(self.pipe, attr_name, type(original).from_config(original.config, shift=shift))
|
| 321 |
+
|
| 322 |
+
if self.schedule_name != "native":
|
| 323 |
+
sigma_func = SCHEDULE_SIGMA_FUNCS[self.schedule_name]
|
| 324 |
+
base = sigma_func(self.steps, self.threshold_noise) if sigma_func is linear_quadratic_sigmas else sigma_func(self.steps)
|
| 325 |
+
for attr_name in self.attr_names:
|
| 326 |
+
scheduler = getattr(self.pipe, attr_name)
|
| 327 |
+
sigmas = time_shift_sigma(base, 1.0, float(scheduler.shift))
|
| 328 |
+
unbound = type(scheduler).set_timesteps
|
| 329 |
+
|
| 330 |
+
def forced(num_inference_steps=None, device=None, sigmas=None, _s=scheduler, _grid=sigmas, _f=unbound):
|
| 331 |
+
return _f(_s, None, device, _grid)
|
| 332 |
+
|
| 333 |
+
scheduler.set_timesteps = forced
|
| 334 |
+
|
| 335 |
+
if self.sampler_name == "euler_ancestral":
|
| 336 |
+
# Separate `torch.Generator` per scheduler (offset seeds) so video and audio ancestral noise don't
|
| 337 |
+
# correlate — each generator advances across every step call to *that* scheduler over the request.
|
| 338 |
+
for offset, attr_name in enumerate(self.attr_names):
|
| 339 |
+
scheduler = getattr(self.pipe, attr_name)
|
| 340 |
+
generator = torch.Generator(device="cpu").manual_seed(self.seed + offset)
|
| 341 |
+
|
| 342 |
+
def stepped(model_output, timestep, sample, return_dict=True, _s=scheduler, _g=generator, **_kwargs):
|
| 343 |
+
return (_euler_ancestral_step(_s, _g, model_output, timestep, sample),)
|
| 344 |
+
|
| 345 |
+
scheduler.step = stepped
|
| 346 |
+
elif self.sampler_name == "er_sde":
|
| 347 |
+
for offset, attr_name in enumerate(self.attr_names):
|
| 348 |
+
scheduler = getattr(self.pipe, attr_name)
|
| 349 |
+
scheduler._er_sde_old_denoised = None
|
| 350 |
+
scheduler._er_sde_old_denoised_d = None
|
| 351 |
+
generator = torch.Generator(device="cpu").manual_seed(self.seed + offset)
|
| 352 |
+
|
| 353 |
+
def stepped(model_output, timestep, sample, return_dict=True, _s=scheduler, _g=generator, **_kwargs):
|
| 354 |
+
return (_er_sde_step(_s, _g, model_output, timestep, sample),)
|
| 355 |
+
|
| 356 |
+
scheduler.step = stepped
|
| 357 |
+
return self
|
| 358 |
+
|
| 359 |
+
def __exit__(self, *_):
|
| 360 |
+
for attr_name, original in self._originals.items():
|
| 361 |
+
current = getattr(self.pipe, attr_name)
|
| 362 |
+
current.__dict__.pop("set_timesteps", None)
|
| 363 |
+
current.__dict__.pop("step", None)
|
| 364 |
+
setattr(self.pipe, attr_name, original)
|
| 365 |
+
return False
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
# ----------------------------------------------------------------------------------------------------------------
|
| 369 |
+
# ImageSharpenKJ(rcas, 0.3)
|
| 370 |
+
# ----------------------------------------------------------------------------------------------------------------
|
| 371 |
+
def rcas(video: torch.Tensor, strength: float, chunk: int = 16) -> torch.Tensor:
|
| 372 |
+
"""AMD FidelityFX **RCAS** — Robust Contrast Adaptive Sharpening — on `(frames, 3, H, W)` in `[0, 1]`.
|
| 373 |
+
|
| 374 |
+
The FidelityFX kernel, which is what `ImageSharpenKJ`'s `rcas` mode is: a 5-tap cross, a sharpening lobe whose
|
| 375 |
+
strength is limited per pixel so the ring it would create cannot leave `[0, 1]`, and a renormalised blend.
|
| 376 |
+
|
| 377 |
+
lobe = clamp(attenuation * min over channels of max(-min / 4*max, -(1 - max) / 4*(1 - min)), -0.1875, 0)
|
| 378 |
+
out = (center + lobe * (n + s + e + w)) / (1 + 4 * lobe)
|
| 379 |
+
|
| 380 |
+
`lobe` is negative, so the neighbours are subtracted: a high-pass with a headroom-aware gain, which is why it
|
| 381 |
+
sharpens MiniMax-H3's slightly soft VAE output without haloing it. PlagueKind's 0.3 is the strength; the note in
|
| 382 |
+
the workflow calls it "very natural" and that matches — the lobe clamp caps it well below a visible ring.
|
| 383 |
+
|
| 384 |
+
Batched over `chunk` frames at a time rather than ComfyUI's one, and written back in place: the clip is already
|
| 385 |
+
resident on the card, but this runs immediately after the denoise loop's allocation peak, and a whole-clip pass at
|
| 386 |
+
the full 1344x768x124 would ask the allocator for ~8 GB of intermediates at exactly the wrong moment.
|
| 387 |
+
"""
|
| 388 |
+
if strength <= 0:
|
| 389 |
+
return video
|
| 390 |
+
|
| 391 |
+
frames, _, height, width = video.shape
|
| 392 |
+
strength = float(strength)
|
| 393 |
+
for start in range(0, frames, chunk):
|
| 394 |
+
center = video[start : start + chunk]
|
| 395 |
+
padded = torch.nn.functional.pad(center, (1, 1, 1, 1), mode="reflect")
|
| 396 |
+
north = padded[:, :, 0:height, 1 : width + 1]
|
| 397 |
+
south = padded[:, :, 2 : height + 2, 1 : width + 1]
|
| 398 |
+
west = padded[:, :, 1 : height + 1, 0:width]
|
| 399 |
+
east = padded[:, :, 1 : height + 1, 2 : width + 2]
|
| 400 |
+
|
| 401 |
+
low = torch.minimum(torch.minimum(torch.minimum(torch.minimum(north, south), west), east), center)
|
| 402 |
+
high = torch.maximum(torch.maximum(torch.maximum(torch.maximum(north, south), west), east), center)
|
| 403 |
+
|
| 404 |
+
hit_min = -low / (high * 4.0 + 1e-6)
|
| 405 |
+
hit_max = -(1.0 - high) / ((1.0 - low) * 4.0 + 1e-6)
|
| 406 |
+
lobe = torch.maximum(hit_min, hit_max).amin(dim=1, keepdim=True)
|
| 407 |
+
lobe = (lobe * strength).clamp_(-0.1875, 0.0)
|
| 408 |
+
del low, high, hit_min, hit_max
|
| 409 |
+
|
| 410 |
+
neighbours = north + south + east + west
|
| 411 |
+
center.copy_(((center + lobe * neighbours) / (1.0 + 4.0 * lobe)).clamp_(0.0, 1.0))
|
| 412 |
+
return video
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
# ----------------------------------------------------------------------------------------------------------------
|
| 416 |
+
# FrameInterpolate(film_net_fp16, multiplier=2)
|
| 417 |
+
# ----------------------------------------------------------------------------------------------------------------
|
| 418 |
+
FILM_REPO = "Comfy-Org/frame_interpolation"
|
| 419 |
+
FILM_FILE = "frame_interpolation/film_net_fp16.safetensors"
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def load_film():
|
| 423 |
+
"""FILM, off the same checkpoint the workflow names. CPU work; `None` on any failure, and the caller skips."""
|
| 424 |
+
from huggingface_hub import hf_hub_download
|
| 425 |
+
from safetensors.torch import load_file
|
| 426 |
+
|
| 427 |
+
from film_net import FILMNet
|
| 428 |
+
|
| 429 |
+
path = hf_hub_download(FILM_REPO, FILM_FILE)
|
| 430 |
+
model = FILMNet()
|
| 431 |
+
model.load_state_dict(load_file(path))
|
| 432 |
+
return model.eval().to(torch.float16)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
@torch.no_grad()
|
| 436 |
+
def interpolate(model, video: torch.Tensor, multiplier: int = 2) -> torch.Tensor:
|
| 437 |
+
"""`multiplier`x frame interpolation of `(frames, 3, H, W)` in `[0, 1]`, FILM, on the card.
|
| 438 |
+
|
| 439 |
+
Mirrors ComfyUI's `FrameInterpolate`: one pass per adjacent pair, the flow computed once per pair and reused for
|
| 440 |
+
every intermediate timestep (`forward_multi_timestep`), and the feature pyramid of frame `i + 1` carried over as
|
| 441 |
+
frame `i` of the next pair — which halves the feature extractions. Output length is
|
| 442 |
+
`(frames - 1) * multiplier + 1`, i.e. 24 fps in, `24 * multiplier` fps out.
|
| 443 |
+
"""
|
| 444 |
+
frames = video.shape[0]
|
| 445 |
+
if model is None or frames < 2 or multiplier < 2:
|
| 446 |
+
return video
|
| 447 |
+
|
| 448 |
+
dtype = torch.float16
|
| 449 |
+
timesteps = [t / multiplier for t in range(1, multiplier)]
|
| 450 |
+
# float16, not the input's float32: the buffer is the largest allocation of the whole post chain (a 2x pass over
|
| 451 |
+
# 124 frames at 1344x768 is 247 of them) and it happens right after the denoise loop's peak.
|
| 452 |
+
out = torch.empty(((frames - 1) * multiplier + 1, *video.shape[1:]), dtype=dtype, device=video.device)
|
| 453 |
+
out[0] = video[0]
|
| 454 |
+
cursor = 1
|
| 455 |
+
|
| 456 |
+
cache: dict = {}
|
| 457 |
+
for index in range(frames - 1):
|
| 458 |
+
first = video[index : index + 1].to(dtype)
|
| 459 |
+
second = video[index + 1 : index + 2].to(dtype)
|
| 460 |
+
cache["img0"] = cache.pop("next") if "next" in cache else model.extract_features(first)
|
| 461 |
+
cache["img1"] = model.extract_features(second)
|
| 462 |
+
cache["next"] = cache["img1"]
|
| 463 |
+
|
| 464 |
+
middles = model.forward_multi_timestep(first, second, timesteps, cache=cache)
|
| 465 |
+
out[cursor : cursor + len(timesteps)] = middles.to(video.dtype).clamp_(0.0, 1.0)
|
| 466 |
+
cursor += len(timesteps)
|
| 467 |
+
out[cursor] = video[index + 1]
|
| 468 |
+
cursor += 1
|
| 469 |
+
|
| 470 |
+
return out
|