Files

177 lines
5.7 KiB
Python

import math
from comfy_api.latest import io
import torch
TIME_SHIFT_TYPES = ["exponential", "linear"]
def _calculate_mu(
seq_length: int,
base_seq_length: int,
max_seq_length: int,
base_shift: float,
max_shift: float,
) -> float:
# diffusers calculate_shift(): the straight line through
# (base_seq_length, base_shift) and (max_seq_length, max_shift), read at
# seq_length.
m = (max_shift - base_shift) / (max_seq_length - base_seq_length)
b = base_shift - m * base_seq_length
return seq_length * m + b
def _time_shift(
mu: float, t: torch.Tensor, time_shift_type: str
) -> torch.Tensor:
# diffusers _time_shift_exponential / _time_shift_linear with sigma fixed at 1.0.
# The exponential branch is the same curve as ComfyUI's shift with shift = e**mu.
if time_shift_type == "exponential":
return math.exp(mu) / (math.exp(mu) + (1.0 / t - 1.0))
return mu / (mu + (1.0 / t - 1.0))
def _stretch_to_terminal(
sigmas: torch.Tensor, shift_terminal: float
) -> torch.Tensor:
# diffusers stretch_shift_to_terminal(): rescales the schedule in (1 - sigma) space
# so the last sigma lands on shift_terminal while the first stays pinned at 1.0.
one_minus_z = 1.0 - sigmas
scale_factor = one_minus_z[-1] / (1.0 - shift_terminal)
return 1.0 - (one_minus_z / scale_factor)
class DynamicShiftScheduler(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="DynamicShiftScheduler",
display_name="🐧 Dynamic Shift Scheduler",
category="SuperNodes/Scheduling",
description="Builds a flow match sigma schedule from a diffusers scheduler config.",
search_aliases=[
"flow match",
"FlowMatchEulerDiscreteScheduler",
"dynamic shifting",
"shift terminal",
"mu",
"diffusers",
"huggingface",
],
inputs=[
io.Int.Input(
"steps",
default=20,
min=2,
max=10_000,
step=1,
),
io.Int.Input(
"seq_length",
default=4096,
min=1,
max=1_000_000,
step=1,
tooltip="Token count of the latent being sampled.",
),
io.Int.Input(
"base_seq_length",
default=256,
min=1,
max=1_000_000,
step=1,
),
io.Int.Input(
"max_seq_length",
default=4096,
min=1,
max=1_000_000,
step=1,
),
io.Float.Input(
"base_shift",
default=0.5,
min=-100.0,
max=100.0,
step=0.01,
),
io.Float.Input(
"max_shift",
default=1.15,
min=-100.0,
max=100.0,
step=0.01,
),
io.Float.Input(
"shift_terminal",
default=0.0,
min=0.0,
max=0.999,
step=0.001,
tooltip="0.0 disables it, matching a config with no shift_terminal.",
),
io.Combo.Input(
"time_shift_type",
options=TIME_SHIFT_TYPES,
),
],
outputs=[
io.Custom("SIGMAS").Output(
tooltip="The flow match sigma schedule."
),
],
)
@classmethod
def execute(
cls,
steps,
seq_length,
base_seq_length,
max_seq_length,
base_shift,
max_shift,
shift_terminal,
time_shift_type,
) -> io.NodeOutput:
if base_seq_length == max_seq_length:
raise ValueError(
f"Invalid config: base_seq_length and max_seq_length are both {base_seq_length}. "
f"The shift line needs two distinct sequence lengths to be defined."
)
mu = _calculate_mu(
seq_length,
base_seq_length,
max_seq_length,
base_shift,
max_shift,
)
# The linear form divides by mu directly rather than by e**mu, so it has no
# useful branch at or below zero the way the exponential form does
if time_shift_type == "linear" and mu <= 0.0:
raise ValueError(
f"Invalid config: linear time_shift_type produced mu = {mu:.4f} at a sequence "
f"length of {seq_length}, but it requires a positive mu. Check base_shift "
f"and max_shift, or switch to exponential."
)
# This grid is what ComfyUI's
# simple scheduler walks, so a config with no shift_terminal comes out
# the same as ModelSamplingFlux plus BasicScheduler on simple.
sigmas = torch.linspace(1.0, 1.0 / steps, steps, dtype=torch.float64)
sigmas = _time_shift(mu, sigmas, time_shift_type)
# A config without shift_terminal leaves the schedule ending on its natural
# final sigma.
if shift_terminal > 0.0:
sigmas = _stretch_to_terminal(sigmas, shift_terminal)
sigmas = torch.cat([sigmas, torch.zeros(1, dtype=torch.float64)])
return io.NodeOutput(sigmas.to(dtype=torch.float32))
NODE = [DynamicShiftScheduler]