cddb07a176
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
196 lines
7.4 KiB
Python
196 lines
7.4 KiB
Python
"""Tests for AnimaSchedulerDriver — the helper that hides per-scheduler API quirks
|
|
(sigmas= vs num_inference_steps=, Heun's doubled timestep array, set_begin_index)
|
|
behind a uniform iteration interface."""
|
|
|
|
import inspect
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from invokeai.app.invocations.anima_denoise import loglinear_timestep_shift
|
|
from invokeai.backend.anima.scheduler_driver import AnimaSchedulerDriver
|
|
from invokeai.backend.flux.schedulers import ANIMA_SCHEDULER_MAP, ANIMA_SHIFT
|
|
|
|
|
|
def _anima_sigmas(num_steps: int) -> list[float]:
|
|
return [loglinear_timestep_shift(ANIMA_SHIFT, 1.0 - i / num_steps) for i in range(num_steps + 1)]
|
|
|
|
|
|
@pytest.mark.parametrize("scheduler_name", ["heun", "dpmpp_2m", "dpmpp_2m_sde", "er_sde"])
|
|
def test_driver_full_schedule_iteration_count(scheduler_name: str) -> None:
|
|
"""For full schedules (no clipping), the driver yields enough iterations to
|
|
cover one full denoise. Heun yields 2N-1 iterations for N user steps."""
|
|
num_steps = 8
|
|
sigmas = _anima_sigmas(num_steps)
|
|
driver = AnimaSchedulerDriver(
|
|
scheduler_name=scheduler_name,
|
|
sigmas=sigmas,
|
|
steps=num_steps,
|
|
denoising_start=0.0,
|
|
denoising_end=1.0,
|
|
device=torch.device("cpu"),
|
|
seed=0,
|
|
)
|
|
iterations = list(driver.iterations())
|
|
if driver.is_heun:
|
|
assert len(iterations) == 2 * num_steps - 1
|
|
else:
|
|
assert len(iterations) == num_steps
|
|
|
|
|
|
@pytest.mark.parametrize("scheduler_name", ["dpmpp_2m", "dpmpp_2m_sde", "er_sde"])
|
|
def test_driver_single_step_schedulers_complete_user_step_every_iteration(scheduler_name: str) -> None:
|
|
"""Non-Heun schedulers report completes_user_step on every iteration."""
|
|
num_steps = 6
|
|
driver = AnimaSchedulerDriver(
|
|
scheduler_name=scheduler_name,
|
|
sigmas=_anima_sigmas(num_steps),
|
|
steps=num_steps,
|
|
denoising_start=0.0,
|
|
denoising_end=1.0,
|
|
device=torch.device("cpu"),
|
|
seed=0,
|
|
)
|
|
user_step_count = sum(1 for it in driver.iterations() if it.completes_user_step)
|
|
assert user_step_count == num_steps
|
|
|
|
|
|
def test_driver_heun_completes_user_step_on_second_order_and_terminal() -> None:
|
|
"""Heun yields one completion per user step: each pair's 2nd-order half plus
|
|
the unpaired terminal 1st-order step (sigma_prev==0)."""
|
|
num_steps = 4
|
|
driver = AnimaSchedulerDriver(
|
|
scheduler_name="heun",
|
|
sigmas=_anima_sigmas(num_steps),
|
|
steps=num_steps,
|
|
denoising_start=0.0,
|
|
denoising_end=1.0,
|
|
device=torch.device("cpu"),
|
|
seed=0,
|
|
)
|
|
# state_in_first_order only toggles once scheduler.step runs, so drive a fake
|
|
# step per iteration to mirror production behaviour.
|
|
completes_flags = []
|
|
for it in driver.iterations():
|
|
completes_flags.append(it.completes_user_step)
|
|
driver.scheduler.step(
|
|
model_output=torch.zeros((1, 1, 1, 4, 4)),
|
|
timestep=it.sched_timestep,
|
|
sample=torch.zeros((1, 1, 1, 4, 4)),
|
|
)
|
|
# N=4 → 7 iterations: indices 1, 3, 5 (SO halves) + 6 (terminal FO) = 4 completions.
|
|
assert sum(completes_flags) == num_steps
|
|
assert completes_flags[-1] is True, "terminal Heun first-order step must complete its user step"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("denoising_start", "denoising_end"),
|
|
[(0.0, 1.0), (0.2, 1.0), (0.0, 0.8), (0.2, 0.8), (0.5, 0.75)],
|
|
)
|
|
def test_driver_dpmpp_clipped_schedule_starts_at_correct_sigma(denoising_start: float, denoising_end: float) -> None:
|
|
"""DPM++ doesn't accept sigmas= on diffusers 0.35.1; the driver's set_begin_index
|
|
fallback must expose a first iteration whose sigma matches the clipped Anima reference.
|
|
|
|
DPM++ constructs its internal flow schedule via ``np.linspace(1, T, N+1)[:-1]`` rather
|
|
than the closed-form Anima loglinear shift, so the leading-edge sigma is offset by up
|
|
to ~2e-3 from the Anima reference. That offset is a property of the scheduler family,
|
|
not the driver — same offset exists in the pre-refactor code path.
|
|
"""
|
|
num_steps = 30
|
|
full_sigmas = _anima_sigmas(num_steps)
|
|
k_start = int(denoising_start * num_steps)
|
|
expected_first_sigma = full_sigmas[k_start]
|
|
|
|
cls, _ = ANIMA_SCHEDULER_MAP["dpmpp_2m"]
|
|
accepts_sigmas = "sigmas" in inspect.signature(cls(num_train_timesteps=1000).set_timesteps).parameters
|
|
|
|
driver = AnimaSchedulerDriver(
|
|
scheduler_name="dpmpp_2m",
|
|
sigmas=full_sigmas[k_start : int(denoising_end * num_steps) + 1] if accepts_sigmas else full_sigmas,
|
|
steps=num_steps,
|
|
denoising_start=denoising_start,
|
|
denoising_end=denoising_end,
|
|
device=torch.device("cpu"),
|
|
seed=0,
|
|
)
|
|
first_iter = next(driver.iterations())
|
|
assert abs(first_iter.sigma_curr - expected_first_sigma) < 2e-3
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("denoising_start", "denoising_end", "steps"),
|
|
[(0.2, 0.8, 30), (0.0, 0.8, 30), (0.2, 1.0, 30), (0.5, 0.75, 20)],
|
|
)
|
|
def test_driver_heun_clipped_schedule_iteration_count(denoising_start: float, denoising_end: float, steps: int) -> None:
|
|
"""Heun clipped schedule: iteration count is 2*(k_end-k_start), clamped so
|
|
denoising_end=1.0 doesn't run past the 2N-1 array."""
|
|
full_sigmas = _anima_sigmas(steps)
|
|
k_start = int(denoising_start * steps)
|
|
k_end = int(denoising_end * steps)
|
|
|
|
driver = AnimaSchedulerDriver(
|
|
scheduler_name="heun",
|
|
sigmas=full_sigmas,
|
|
steps=steps,
|
|
denoising_start=denoising_start,
|
|
denoising_end=denoising_end,
|
|
device=torch.device("cpu"),
|
|
seed=0,
|
|
)
|
|
|
|
# If Heun's set_timesteps accepts sigmas=, the driver will pass the full schedule directly
|
|
# and yield 2*steps-1 iterations regardless of clipping. The set_begin_index path applies
|
|
# only when sigmas= is unsupported.
|
|
accepts_sigmas = "sigmas" in inspect.signature(driver.scheduler.set_timesteps).parameters
|
|
if accepts_sigmas:
|
|
# Driver took the sigma-passing path; sigmas were not pre-clipped here, so the count
|
|
# reflects the full schedule.
|
|
assert driver.num_iterations == 2 * steps - 1
|
|
return
|
|
|
|
expected = min(2 * (k_end - k_start), len(driver.scheduler.timesteps) - driver.begin_index)
|
|
assert driver.num_iterations == expected
|
|
assert driver.begin_index == 2 * k_start
|
|
|
|
|
|
def test_driver_terminal_sigma_prev_is_zero() -> None:
|
|
"""The last iteration's sigma_prev must be 0.0 (terminal noise level)."""
|
|
driver = AnimaSchedulerDriver(
|
|
scheduler_name="dpmpp_2m",
|
|
sigmas=_anima_sigmas(8),
|
|
steps=8,
|
|
denoising_start=0.0,
|
|
denoising_end=1.0,
|
|
device=torch.device("cpu"),
|
|
seed=0,
|
|
)
|
|
last_iter = list(driver.iterations())[-1]
|
|
assert last_iter.sigma_prev == 0.0
|
|
|
|
|
|
def test_driver_seed_determinism() -> None:
|
|
"""Same seed → identical step_generator state → reproducible SDE noise."""
|
|
sigmas = _anima_sigmas(8)
|
|
driver_a = AnimaSchedulerDriver(
|
|
scheduler_name="er_sde",
|
|
sigmas=sigmas,
|
|
steps=8,
|
|
denoising_start=0.0,
|
|
denoising_end=1.0,
|
|
device=torch.device("cpu"),
|
|
seed=42,
|
|
)
|
|
driver_b = AnimaSchedulerDriver(
|
|
scheduler_name="er_sde",
|
|
sigmas=sigmas,
|
|
steps=8,
|
|
denoising_start=0.0,
|
|
denoising_end=1.0,
|
|
device=torch.device("cpu"),
|
|
seed=42,
|
|
)
|
|
# Same seed → same first random draw.
|
|
a = torch.randn((1, 4), generator=driver_a.step_generator)
|
|
b = torch.randn((1, 4), generator=driver_b.step_generator)
|
|
assert torch.equal(a, b)
|